fix: sync detail scale target flow
This commit is contained in:
parent
bf8410a6d4
commit
088d2c1b74
@ -28,7 +28,6 @@ import {
|
||||
ImagePlus,
|
||||
Loader2,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Sparkles,
|
||||
Upload,
|
||||
UserRound,
|
||||
@ -247,7 +246,7 @@ type DetailResultLocalEditTarget = {
|
||||
};
|
||||
|
||||
type DetailResultLocalEditMode = "local_edit" | "scale_adjust";
|
||||
type DetailResultScalePercent = -30 | -20 | -10 | 10 | 20 | 30 | 50 | 100;
|
||||
type DetailResultScalePercent = number;
|
||||
|
||||
type DetailResultLocalEditRect = {
|
||||
x: number;
|
||||
@ -258,12 +257,15 @@ type DetailResultLocalEditRect = {
|
||||
|
||||
type DetailResultLocalEditRegionHandle = "move" | "nw" | "ne" | "sw" | "se";
|
||||
|
||||
type DetailResultLocalEditRegionInteractionTarget = "source" | "scaleTarget";
|
||||
|
||||
type DetailResultLocalEditRegionInteraction = {
|
||||
handle: DetailResultLocalEditRegionHandle;
|
||||
pointerId: number;
|
||||
regionId: string;
|
||||
startPoint: { x: number; y: number };
|
||||
startRect: DetailResultLocalEditRect;
|
||||
target: DetailResultLocalEditRegionInteractionTarget;
|
||||
};
|
||||
|
||||
type DetailResultLocalEditSubmitParams = {
|
||||
@ -291,17 +293,9 @@ const MAX_AGENT_MODE_QUERY_LENGTH = 64;
|
||||
const MAX_AGENT_MODE_REFERENCE_COUNT = 40;
|
||||
const DETAIL_LOCAL_EDIT_REFERENCE_MAX_UPLOAD_BYTES = 30 * 1024 * 1024;
|
||||
const REPLACEMENT_RERUN_MAX_REFERENCE_IMAGES = 4;
|
||||
const DETAIL_SCALE_ADJUST_OPTIONS: DetailResultScalePercent[] = [
|
||||
-30,
|
||||
-20,
|
||||
-10,
|
||||
10,
|
||||
20,
|
||||
30,
|
||||
50,
|
||||
100,
|
||||
];
|
||||
const DETAIL_LOCAL_EDIT_MIN_RECT_RATIO = 0.025;
|
||||
const DETAIL_SCALE_TARGET_MIN_PERCENT = -80;
|
||||
const DETAIL_SCALE_TARGET_MAX_PERCENT = 200;
|
||||
const DIRECT_MODEL_UPLOAD_SLOT_COUNT = 4;
|
||||
const PRODUCT_DETAIL_CHANNEL_OPTIONS: Array<{
|
||||
value: AgentWorkflowProductDetailChannel;
|
||||
@ -852,18 +846,22 @@ function getDetailScaleAdjustLabel(scalePercent: DetailResultScalePercent) {
|
||||
}
|
||||
|
||||
function getDetailScaleAdjustTargetLabel(scalePercent: DetailResultScalePercent) {
|
||||
return `${Math.round((1 + scalePercent / 100) * 100)}%`;
|
||||
return `${Math.max(5, Math.round((1 + scalePercent / 100) * 100))}%`;
|
||||
}
|
||||
|
||||
function getDetailScaleAdjustRegionPrompt(scalePercent: DetailResultScalePercent | null) {
|
||||
function getDetailScaleAdjustRegionPrompt(
|
||||
scalePercent: DetailResultScalePercent | null,
|
||||
hasTargetPosition = false
|
||||
) {
|
||||
if (!scalePercent) return "比例调整";
|
||||
const actionLabel = getDetailScaleAdjustLabel(scalePercent);
|
||||
const targetLabel = getDetailScaleAdjustTargetLabel(scalePercent);
|
||||
const scaleVerb = scalePercent > 0 ? "放大" : "缩小";
|
||||
return [
|
||||
`对框选范围内的主物体执行等比${actionLabel},调整后主体视觉尺寸必须约为原来的 ${targetLabel},需要肉眼明显看出已经${scaleVerb}。`,
|
||||
"必须锁定主体自身原始宽高比和透视形体,宽度与高度用同一个倍率同步变化;严禁宽度变窄但高度变高、严禁横向挤压或纵向拉伸。",
|
||||
"框选区域只用于识别要调整的主体,不是放大后的边界;放大时主体可以自然超出原始框选范围,并由周边背景补全衔接。",
|
||||
hasTargetPosition
|
||||
? "主体框只用于识别要调整的主体;青色目标框表示调整后的目标大小和目标位置,放大后的主体可以自然超出主体框,并由周边背景补全衔接。"
|
||||
: "框选区域只用于识别要调整的主体,不是放大后的边界;放大时主体可以自然超出原始框选范围,并由周边背景补全衔接。",
|
||||
"只改变目标主体的尺寸,不改变主体外观、颜色、材质、图案和文字内容;不要把整个框选矩形当成图片块缩放。",
|
||||
scalePercent < 0
|
||||
? "主体缩小后露出的背景、桌面、墙面、阴影和遮挡关系必须自然补全。"
|
||||
@ -871,6 +869,23 @@ function getDetailScaleAdjustRegionPrompt(scalePercent: DetailResultScalePercent
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function deriveDetailScalePercentFromRects(
|
||||
sourceRect: DetailResultLocalEditRect,
|
||||
targetRect: DetailResultLocalEditRect | null | undefined
|
||||
) {
|
||||
if (!targetRect || sourceRect.width <= 0 || sourceRect.height <= 0) {
|
||||
return 0;
|
||||
}
|
||||
const widthRatio = Math.max(0.05, targetRect.width / sourceRect.width);
|
||||
const heightRatio = Math.max(0.05, targetRect.height / sourceRect.height);
|
||||
const uniformRatio = Math.sqrt(widthRatio * heightRatio);
|
||||
const percent = Math.round((uniformRatio - 1) * 100);
|
||||
return Math.min(
|
||||
DETAIL_SCALE_TARGET_MAX_PERCENT,
|
||||
Math.max(DETAIL_SCALE_TARGET_MIN_PERCENT, percent)
|
||||
);
|
||||
}
|
||||
|
||||
function DetailResultLocalEditDialog({
|
||||
previewResult,
|
||||
submitting,
|
||||
@ -894,8 +909,7 @@ function DetailResultLocalEditDialog({
|
||||
const [regionInteraction, setRegionInteraction] =
|
||||
useState<DetailResultLocalEditRegionInteraction | null>(null);
|
||||
const [uploadingRegionId, setUploadingRegionId] = useState<string | null>(null);
|
||||
const [selectedScalePercent, setSelectedScalePercent] =
|
||||
useState<DetailResultScalePercent | null>(null);
|
||||
const [drawingTargetRegionId, setDrawingTargetRegionId] = useState<string | null>(null);
|
||||
const dragStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const editSurfaceRef = useRef<HTMLDivElement | null>(null);
|
||||
const mode: DetailResultLocalEditMode =
|
||||
@ -906,6 +920,10 @@ function DetailResultLocalEditDialog({
|
||||
? previewResult
|
||||
: null;
|
||||
const isBusy = submitting || Boolean(uploadingRegionId);
|
||||
const pendingScaleTargetRegion = isScaleAdjustMode
|
||||
? regions.find((region) => !region.targetRect)
|
||||
: undefined;
|
||||
const isDrawingScaleTarget = isScaleAdjustMode && Boolean(drawingTargetRegionId);
|
||||
|
||||
useEffect(() => {
|
||||
setRegions([]);
|
||||
@ -913,7 +931,7 @@ function DetailResultLocalEditDialog({
|
||||
setActiveRegionId(null);
|
||||
setRegionInteraction(null);
|
||||
setUploadingRegionId(null);
|
||||
setSelectedScalePercent(null);
|
||||
setDrawingTargetRegionId(null);
|
||||
dragStartRef.current = null;
|
||||
}, [target?.cardId, target?.imageUrl, target?.mode]);
|
||||
|
||||
@ -970,15 +988,40 @@ function DetailResultLocalEditDialog({
|
||||
}, []);
|
||||
|
||||
const updateRegionRect = useCallback(
|
||||
(regionId: string, rect: DetailResultLocalEditRect) => {
|
||||
(
|
||||
regionId: string,
|
||||
rect: DetailResultLocalEditRect,
|
||||
targetKind: DetailResultLocalEditRegionInteractionTarget = "source"
|
||||
) => {
|
||||
const nextRect = clampRegionRect(rect);
|
||||
setRegions((current) =>
|
||||
current.map((region) =>
|
||||
region.id === regionId ? { ...region, rect: nextRect } : region
|
||||
region.id === regionId
|
||||
? targetKind === "scaleTarget"
|
||||
? {
|
||||
...region,
|
||||
targetRect: nextRect,
|
||||
prompt: getDetailScaleAdjustRegionPrompt(
|
||||
deriveDetailScalePercentFromRects(region.rect, nextRect),
|
||||
true
|
||||
),
|
||||
}
|
||||
: {
|
||||
...region,
|
||||
rect: nextRect,
|
||||
prompt:
|
||||
isScaleAdjustMode && region.targetRect
|
||||
? getDetailScaleAdjustRegionPrompt(
|
||||
deriveDetailScalePercentFromRects(nextRect, region.targetRect),
|
||||
true
|
||||
)
|
||||
: region.prompt,
|
||||
}
|
||||
: region
|
||||
)
|
||||
);
|
||||
},
|
||||
[clampRegionRect]
|
||||
[clampRegionRect, isScaleAdjustMode]
|
||||
);
|
||||
|
||||
const calculateInteractionRect = useCallback(
|
||||
@ -1138,7 +1181,10 @@ function DetailResultLocalEditDialog({
|
||||
regions.map((region) => ({
|
||||
...region,
|
||||
prompt: isScaleAdjustMode
|
||||
? getDetailScaleAdjustRegionPrompt(selectedScalePercent)
|
||||
? getDetailScaleAdjustRegionPrompt(
|
||||
deriveDetailScalePercentFromRects(region.rect, region.targetRect),
|
||||
Boolean(region.targetRect)
|
||||
)
|
||||
: region.prompt.trim(),
|
||||
referenceImages: (region.referenceImages || []).slice(
|
||||
0,
|
||||
@ -1160,8 +1206,18 @@ function DetailResultLocalEditDialog({
|
||||
return;
|
||||
}
|
||||
if (isScaleAdjustMode) {
|
||||
if (!selectedScalePercent) {
|
||||
toast.error("请选择放大或缩小比例");
|
||||
const missingTargetRegion = validRegions.find((region) => !region.targetRect);
|
||||
if (missingTargetRegion) {
|
||||
setActiveRegionId(missingTargetRegion.id);
|
||||
toast.error(`请为区域 ${missingTargetRegion.index} 框选青色目标框`);
|
||||
return;
|
||||
}
|
||||
const unchangedRegion = validRegions.find(
|
||||
(region) => deriveDetailScalePercentFromRects(region.rect, region.targetRect) === 0
|
||||
);
|
||||
if (unchangedRegion) {
|
||||
setActiveRegionId(unchangedRegion.id);
|
||||
toast.error(`区域 ${unchangedRegion.index} 的目标框大小变化不明显,请重新框选`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@ -1175,11 +1231,17 @@ function DetailResultLocalEditDialog({
|
||||
onSubmit({
|
||||
imageUrl: target.imageUrl,
|
||||
mode,
|
||||
scalePercent: isScaleAdjustMode ? selectedScalePercent || undefined : undefined,
|
||||
scalePercent:
|
||||
isScaleAdjustMode && validRegions[0]
|
||||
? deriveDetailScalePercentFromRects(
|
||||
validRegions[0].rect,
|
||||
validRegions[0].targetRect
|
||||
) || undefined
|
||||
: undefined,
|
||||
title: target.title,
|
||||
regions: validRegions,
|
||||
});
|
||||
}, [isScaleAdjustMode, mode, onSubmit, regions, selectedScalePercent, target, uploadingRegionId]);
|
||||
}, [isScaleAdjustMode, mode, onSubmit, regions, target, uploadingRegionId]);
|
||||
|
||||
const completeDraftRegion = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
@ -1188,8 +1250,10 @@ function DetailResultLocalEditDialog({
|
||||
|
||||
const point = getPoint(event);
|
||||
const nextRect = createRectFromPoints(start, point);
|
||||
const targetRegionId = drawingTargetRegionId;
|
||||
dragStartRef.current = null;
|
||||
setDraftRect(null);
|
||||
setDrawingTargetRegionId(null);
|
||||
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
@ -1203,13 +1267,32 @@ function DetailResultLocalEditDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isScaleAdjustMode && targetRegionId) {
|
||||
setRegions((current) =>
|
||||
reindexLocalEditRegions(
|
||||
current.map((region) =>
|
||||
region.id === targetRegionId
|
||||
? {
|
||||
...region,
|
||||
targetRect: nextRect,
|
||||
prompt: getDetailScaleAdjustRegionPrompt(
|
||||
deriveDetailScalePercentFromRects(region.rect, nextRect),
|
||||
true
|
||||
),
|
||||
}
|
||||
: region
|
||||
)
|
||||
)
|
||||
);
|
||||
setActiveRegionId(targetRegionId);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRegion: LocalEditRegion = {
|
||||
id: createLocalEditRegionId(),
|
||||
index: regions.length + 1,
|
||||
color: LOCAL_EDIT_COLORS[regions.length % LOCAL_EDIT_COLORS.length],
|
||||
prompt: isScaleAdjustMode
|
||||
? getDetailScaleAdjustRegionPrompt(selectedScalePercent)
|
||||
: "",
|
||||
prompt: isScaleAdjustMode ? getDetailScaleAdjustRegionPrompt(null) : "",
|
||||
rect: nextRect,
|
||||
referenceImages: [],
|
||||
createdAt: Date.now(),
|
||||
@ -1220,14 +1303,16 @@ function DetailResultLocalEditDialog({
|
||||
});
|
||||
setActiveRegionId(nextRegion.id);
|
||||
},
|
||||
[createRectFromPoints, getPoint, isScaleAdjustMode, regions.length, selectedScalePercent]
|
||||
[createRectFromPoints, drawingTargetRegionId, getPoint, isScaleAdjustMode, regions.length]
|
||||
);
|
||||
|
||||
const handleRegionInteractionStart = useCallback(
|
||||
(
|
||||
event: React.PointerEvent<HTMLButtonElement>,
|
||||
region: LocalEditRegion,
|
||||
fallbackHandle: DetailResultLocalEditRegionHandle = "move"
|
||||
fallbackHandle: DetailResultLocalEditRegionHandle = "move",
|
||||
targetKind: DetailResultLocalEditRegionInteractionTarget = "source",
|
||||
targetRect?: DetailResultLocalEditRect | null
|
||||
) => {
|
||||
if (isBusy) return;
|
||||
const targetElement = event.target as HTMLElement | null;
|
||||
@ -1239,13 +1324,15 @@ function DetailResultLocalEditDialog({
|
||||
setActiveRegionId(region.id);
|
||||
dragStartRef.current = null;
|
||||
setDraftRect(null);
|
||||
setDrawingTargetRegionId(null);
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
setRegionInteraction({
|
||||
handle,
|
||||
pointerId: event.pointerId,
|
||||
regionId: region.id,
|
||||
startPoint: getPoint(event),
|
||||
startRect: region.rect,
|
||||
startRect: targetKind === "scaleTarget" ? targetRect || region.rect : region.rect,
|
||||
target: targetKind,
|
||||
});
|
||||
},
|
||||
[getPoint, isBusy]
|
||||
@ -1262,7 +1349,11 @@ function DetailResultLocalEditDialog({
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
updateRegionRect(regionId, calculateInteractionRect(regionInteraction, getPoint(event)));
|
||||
updateRegionRect(
|
||||
regionId,
|
||||
calculateInteractionRect(regionInteraction, getPoint(event)),
|
||||
regionInteraction.target
|
||||
);
|
||||
},
|
||||
[calculateInteractionRect, getPoint, regionInteraction, updateRegionRect]
|
||||
);
|
||||
@ -1276,7 +1367,11 @@ function DetailResultLocalEditDialog({
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
updateRegionRect(regionId, calculateInteractionRect(regionInteraction, getPoint(event)));
|
||||
updateRegionRect(
|
||||
regionId,
|
||||
calculateInteractionRect(regionInteraction, getPoint(event)),
|
||||
regionInteraction.target
|
||||
);
|
||||
setRegionInteraction(null);
|
||||
}
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
@ -1292,7 +1387,7 @@ function DetailResultLocalEditDialog({
|
||||
: "处理完成后会加入 AI详情结果区。";
|
||||
const dialogTitle = isScaleAdjustMode ? "比例调整详情图" : "局部编辑详情图";
|
||||
const dialogDescription = isScaleAdjustMode
|
||||
? "框选要调整比例的物体,选择固定放大或缩小幅度后生成。"
|
||||
? "先框选要调整比例的物体,再框选目标大小和位置后生成。"
|
||||
: "在图片上拖拽框选编辑位置,可添加多个区域,并为每个区域上传专属参考图。";
|
||||
|
||||
return (
|
||||
@ -1329,24 +1424,30 @@ function DetailResultLocalEditDialog({
|
||||
updateDraftRect(event);
|
||||
dragStartRef.current = null;
|
||||
setDraftRect(null);
|
||||
setDrawingTargetRegionId(null);
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
if (event.button !== 0 || isBusy) return;
|
||||
if (regions.length >= LOCAL_EDIT_MAX_REGIONS) {
|
||||
toast.info(
|
||||
`${isScaleAdjustMode ? "比例调整" : "局部编辑"}最多支持 ${LOCAL_EDIT_MAX_REGIONS} 个区域`
|
||||
);
|
||||
return;
|
||||
}
|
||||
onPointerDown={(event) => {
|
||||
if (event.button !== 0 || isBusy) return;
|
||||
const targetRegion = isScaleAdjustMode
|
||||
? regions.find((region) => !region.targetRect)
|
||||
: undefined;
|
||||
if (!targetRegion && regions.length >= LOCAL_EDIT_MAX_REGIONS) {
|
||||
toast.info(
|
||||
`${isScaleAdjustMode ? "比例调整" : "局部编辑"}最多支持 ${LOCAL_EDIT_MAX_REGIONS} 个区域`
|
||||
);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
const point = getPoint(event);
|
||||
dragStartRef.current = point;
|
||||
setDraftRect({ x: point.x, y: point.y, width: 0.01, height: 0.01 });
|
||||
}}
|
||||
const point = getPoint(event);
|
||||
dragStartRef.current = point;
|
||||
setDrawingTargetRegionId(targetRegion?.id || null);
|
||||
if (targetRegion) setActiveRegionId(targetRegion.id);
|
||||
setDraftRect({ x: point.x, y: point.y, width: 0.01, height: 0.01 });
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
if (!dragStartRef.current) return;
|
||||
event.preventDefault();
|
||||
@ -1446,11 +1547,111 @@ function DetailResultLocalEditDialog({
|
||||
))
|
||||
: null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
{isScaleAdjustMode
|
||||
? regions
|
||||
.filter((region) => region.targetRect)
|
||||
.map((region) => {
|
||||
const targetRect = region.targetRect as DetailResultLocalEditRect;
|
||||
const scalePercent = deriveDetailScalePercentFromRects(
|
||||
region.rect,
|
||||
targetRect
|
||||
);
|
||||
const isActive = region.id === activeRegionId;
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"absolute z-20 rounded-sm border-2 border-dashed border-cyan-300 bg-cyan-300/10 text-left transition",
|
||||
regionInteraction?.regionId === region.id &&
|
||||
regionInteraction.target === "scaleTarget"
|
||||
? "cursor-grabbing"
|
||||
: "cursor-grab",
|
||||
isActive ? "shadow-[0_0_0_3px_rgba(103,232,249,0.28)]" : ""
|
||||
)}
|
||||
key={`target-${region.id}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setActiveRegionId(region.id);
|
||||
}}
|
||||
onPointerCancel={(event) =>
|
||||
handleRegionInteractionEnd(event, region.id)
|
||||
}
|
||||
onPointerDown={(event) =>
|
||||
handleRegionInteractionStart(
|
||||
event,
|
||||
region,
|
||||
"move",
|
||||
"scaleTarget",
|
||||
targetRect
|
||||
)
|
||||
}
|
||||
onPointerMove={(event) =>
|
||||
handleRegionInteractionMove(event, region.id)
|
||||
}
|
||||
onPointerUp={(event) =>
|
||||
handleRegionInteractionEnd(event, region.id)
|
||||
}
|
||||
style={{
|
||||
left: `${targetRect.x * 100}%`,
|
||||
top: `${targetRect.y * 100}%`,
|
||||
width: `${targetRect.width * 100}%`,
|
||||
height: `${targetRect.height * 100}%`,
|
||||
}}
|
||||
title={`目标框:${getDetailScaleAdjustLabel(scalePercent)}`}
|
||||
type="button"
|
||||
>
|
||||
<span className="absolute -right-2 -top-2 rounded-full border border-cyan-200/70 bg-cyan-950/85 px-2 py-0.5 text-[11px] font-semibold text-cyan-50 shadow-lg">
|
||||
目标 {scalePercent > 0 ? `+${scalePercent}%` : `${scalePercent}%`}
|
||||
</span>
|
||||
{isActive
|
||||
? (
|
||||
[
|
||||
{
|
||||
handle: "nw",
|
||||
className:
|
||||
"-left-[6px] -top-[6px] cursor-nwse-resize",
|
||||
},
|
||||
{
|
||||
handle: "ne",
|
||||
className:
|
||||
"-right-[6px] -top-[6px] cursor-nesw-resize",
|
||||
},
|
||||
{
|
||||
handle: "sw",
|
||||
className:
|
||||
"-bottom-[6px] -left-[6px] cursor-nesw-resize",
|
||||
},
|
||||
{
|
||||
handle: "se",
|
||||
className:
|
||||
"-bottom-[6px] -right-[6px] cursor-nwse-resize",
|
||||
},
|
||||
] as const
|
||||
).map((item) => (
|
||||
<span
|
||||
className={cn(
|
||||
"absolute h-3 w-3 rounded-[2px] border-2 border-cyan-300 bg-white shadow-[0_3px_10px_rgba(0,0,0,0.28)]",
|
||||
item.className
|
||||
)}
|
||||
data-detail-region-handle={item.handle}
|
||||
key={item.handle}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</button>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
{draftRect ? (
|
||||
<div
|
||||
className="pointer-events-none absolute border-2 border-sky-300 bg-sky-400/20 shadow-[0_0_0_9999px_rgba(0,0,0,0.24)]"
|
||||
className={cn(
|
||||
"pointer-events-none absolute border-2 shadow-[0_0_0_9999px_rgba(0,0,0,0.24)]",
|
||||
isDrawingScaleTarget
|
||||
? "border-dashed border-cyan-300 bg-cyan-400/18"
|
||||
: "border-sky-300 bg-sky-400/20"
|
||||
)}
|
||||
style={{
|
||||
left: `${draftRect.x * 100}%`,
|
||||
top: `${draftRect.y * 100}%`,
|
||||
@ -1469,47 +1670,25 @@ function DetailResultLocalEditDialog({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-col gap-3">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.04] p-3 text-sm text-white/58">
|
||||
{isScaleAdjustMode
|
||||
? `拖拽框选物体,建议留出少量周边背景,最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域。${completionCopy}`
|
||||
: `拖拽图片添加区域。最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域,每个区域最多 ${LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION} 张参考图。${completionCopy}`}
|
||||
</div>
|
||||
{!activePreviewResult && isScaleAdjustMode ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.035] p-3">
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-white/38">
|
||||
Scale
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{DETAIL_SCALE_ADJUST_OPTIONS.map((option) => {
|
||||
const active = selectedScalePercent === option;
|
||||
const Icon = option > 0 ? Maximize2 : Minimize2;
|
||||
const labelAction = option > 0 ? "放大" : "缩小";
|
||||
const labelValue = `${Math.abs(option)}%`;
|
||||
return (
|
||||
<button
|
||||
aria-label={getDetailScaleAdjustLabel(option)}
|
||||
className={cn(
|
||||
"grid h-12 grid-cols-[16px_minmax(0,1fr)] items-center gap-1 rounded-full border px-2 text-xs font-semibold leading-none transition disabled:cursor-not-allowed disabled:opacity-45",
|
||||
active
|
||||
? "border-emerald-300/70 bg-emerald-400/18 text-emerald-50"
|
||||
: "border-white/10 bg-black/20 text-white/68 hover:border-white/24 hover:bg-white/[0.08] hover:text-white"
|
||||
)}
|
||||
disabled={isBusy}
|
||||
key={option}
|
||||
onClick={() => setSelectedScalePercent(option)}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 justify-self-center" />
|
||||
<span className="flex min-w-0 flex-col items-center justify-center gap-0.5 text-center leading-[1.08]">
|
||||
<span>{labelAction}</span>
|
||||
<span>{labelValue}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.04] p-3 text-sm text-white/58">
|
||||
{isScaleAdjustMode
|
||||
? `先拖拽框选主体,再拖拽青色目标框设置大小和位置,最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域。${completionCopy}`
|
||||
: `拖拽图片添加区域。最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域,每个区域最多 ${LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION} 张参考图。${completionCopy}`}
|
||||
</div>
|
||||
{!activePreviewResult && isScaleAdjustMode ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.035] p-3 text-sm text-white/58">
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.18em] text-white/38">
|
||||
Target Box
|
||||
</div>
|
||||
<div className="leading-6">
|
||||
{pendingScaleTargetRegion
|
||||
? `继续在左侧为区域 ${pendingScaleTargetRegion.index} 拖拽青色目标框。`
|
||||
: regions.length > 0
|
||||
? "目标框已补齐,可拖动或拉伸青色框继续调整位置和大小。"
|
||||
: "先在左侧拖拽框选要调整比例的主体。"}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{activePreviewResult ? (
|
||||
<div className="flex min-h-[260px] flex-col justify-center rounded-2xl border border-emerald-200/18 bg-emerald-400/[0.07] p-4">
|
||||
<div className="mb-2 flex items-center gap-2 text-sm font-semibold text-emerald-50">
|
||||
@ -1521,11 +1700,11 @@ function DetailResultLocalEditDialog({
|
||||
</div>
|
||||
</div>
|
||||
) : regions.length === 0 ? (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-2xl border border-dashed border-white/12 bg-white/[0.02] px-5 text-center text-sm text-white/38">
|
||||
{isScaleAdjustMode
|
||||
? "在左侧图片上拖拽框选物体后,选择一个比例即可生成。"
|
||||
: "在左侧图片上拖拽框选后,这里会显示区域提示词和参考图。"}
|
||||
</div>
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-2xl border border-dashed border-white/12 bg-white/[0.02] px-5 text-center text-sm text-white/38">
|
||||
{isScaleAdjustMode
|
||||
? "在左侧图片上先框主体,再框青色目标大小和位置。"
|
||||
: "在左侧图片上拖拽框选后,这里会显示区域提示词和参考图。"}
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[52vh] space-y-3 overflow-y-auto pr-1">
|
||||
{regions.map((region) => {
|
||||
@ -1556,12 +1735,17 @@ function DetailResultLocalEditDialog({
|
||||
<span className="truncate text-sm font-semibold text-white">
|
||||
区域 {region.index}
|
||||
</span>
|
||||
{isScaleAdjustMode ? (
|
||||
<span className="text-xs text-white/36">
|
||||
{selectedScalePercent
|
||||
? getDetailScaleAdjustLabel(selectedScalePercent)
|
||||
: "待选比例"}
|
||||
</span>
|
||||
{isScaleAdjustMode ? (
|
||||
<span className="text-xs text-white/36">
|
||||
{region.targetRect
|
||||
? getDetailScaleAdjustLabel(
|
||||
deriveDetailScalePercentFromRects(
|
||||
region.rect,
|
||||
region.targetRect
|
||||
)
|
||||
)
|
||||
: "待框目标框"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-white/36">
|
||||
参考图 {references.length}/
|
||||
@ -1706,12 +1890,12 @@ function DetailResultLocalEditDialog({
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-white text-black hover:bg-white/90"
|
||||
disabled={
|
||||
submitting ||
|
||||
Boolean(uploadingRegionId) ||
|
||||
regions.length === 0 ||
|
||||
(isScaleAdjustMode && !selectedScalePercent)
|
||||
}
|
||||
disabled={
|
||||
submitting ||
|
||||
Boolean(uploadingRegionId) ||
|
||||
regions.length === 0 ||
|
||||
(isScaleAdjustMode && regions.some((region) => !region.targetRect))
|
||||
}
|
||||
onClick={handleSubmit}
|
||||
type="button"
|
||||
>
|
||||
@ -6334,6 +6518,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
color: region.color,
|
||||
prompt: region.prompt,
|
||||
rect: region.rect,
|
||||
targetRect: region.targetRect,
|
||||
referenceImages: (region.referenceImages || []).map((reference) => ({
|
||||
id: reference.id,
|
||||
url: reference.url,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user