fix(agent): restore detail replacement result pane

This commit is contained in:
134 2026-08-01 14:43:49 +08:00
parent 87bbcf0ccd
commit c988f2dee0
3 changed files with 153 additions and 94 deletions

View File

@ -114,6 +114,10 @@ import {
getAgentGenerationAcceptanceRemainingMs,
isAgentGenerationRequestAccepted,
} from "@/lib/agents/pending-generation-guard";
import {
isReplacementResultMode,
shouldShowAgentGridResultSection,
} from "@/lib/agents/generation-result-visibility";
import {
defaultAgentWorkflowBusinessModeSettings,
normalizeAgentWorkflowBusinessModeSettings,
@ -3118,6 +3122,9 @@ export function AgentGenerationWorkspaceDarkV2({
const selectNodes = useCanvasStore((state) => state.selectNodes);
const requestImageNodeAction = useCanvasStore((state) => state.requestImageNodeAction);
const workspaceRootRef = useRef<HTMLDivElement | null>(null);
const resultsSectionRef = useRef<HTMLElement | null>(null);
const replacementResultsContentRef = useRef<HTMLDivElement | null>(null);
const lastAutoRevealedResultRequestRef = useRef<string | null>(null);
const fileInputRefs = useRef<Array<HTMLInputElement | null>>([]);
const detailSlotInputRefs = useRef<Array<HTMLInputElement | null>>([]);
const hiddenPromptRef = useRef<string | null>(null);
@ -4804,17 +4811,9 @@ export function AgentGenerationWorkspaceDarkV2({
userMessageCount,
]);
const isAnyGenerationRunning = transportIsGenerating || Boolean(pendingGeneration);
const isSceneResultModeGenerating =
(activeResultModeType === "scene_generation" &&
pendingGeneration?.modeType === "scene_generation") ||
(activeResultModeType === currentResultModeType &&
currentResultModeType === "scene_generation" &&
transportIsGenerating);
const isDetailResultModeGenerating =
pendingGeneration?.modeType === "detail_replacement" ||
(currentResultModeType === "detail_replacement" && transportIsGenerating);
const isDetailGenerationResultModeGenerating =
pendingGeneration?.modeType === "detail_generation";
const isSkuResultModeGenerating =
pendingGeneration?.modeType === "sku_replacement" ||
(currentResultModeType === "sku_replacement" && transportIsGenerating);
@ -5210,11 +5209,14 @@ export function AgentGenerationWorkspaceDarkV2({
resultCompareSourceByUrl,
]
);
const shouldShowGridResultSection =
activeResultModeUrls.length > 0 ||
((activeResultModeType === "scene_generation" && isSceneResultModeGenerating) ||
(activeResultModeType === "detail_generation" &&
(isDetailGenerationResultModeGenerating || hasDetailGenerationPlaceholderState)));
const shouldShowGridResultSection = shouldShowAgentGridResultSection({
activeModeType: activeResultModeType,
activeResultCount: activeResultModeUrls.length,
currentModeType: currentResultModeType,
pendingModeType: pendingGeneration?.modeType,
transportIsGenerating,
hasDetailGenerationPlaceholderState,
});
const hasBrandModelSelection = Boolean(
selectedBrandModel?.imageUrl || referenceImages.some((image) => image.source === "brandModel")
);
@ -5388,6 +5390,29 @@ export function AgentGenerationWorkspaceDarkV2({
}
}, [pendingGeneration, pendingGenerationProgressCount]);
useEffect(() => {
if (!pendingGeneration || !isReplacementResultMode(pendingGeneration.modeType)) {
return;
}
const requestKey =
pendingGeneration.requestMessageId ||
`${pendingGeneration.chatId}:${pendingGeneration.timestamp}`;
if (lastAutoRevealedResultRequestRef.current === requestKey) {
return;
}
lastAutoRevealedResultRequestRef.current = requestKey;
const frameId = window.requestAnimationFrame(() => {
const target = replacementResultsContentRef.current || resultsSectionRef.current;
target?.scrollIntoView({
behavior: "smooth",
block: "start",
});
});
return () => window.cancelAnimationFrame(frameId);
}, [pendingGeneration]);
useEffect(() => {
if (!pendingGeneration || !chatError) {
return;
@ -9685,9 +9710,10 @@ export function AgentGenerationWorkspaceDarkV2({
) : null}
<motion.section
ref={resultsSectionRef}
id="agent-anchor-results"
className={cn(
"relative flex min-h-[620px] flex-col rounded-[28px] border border-white/12 bg-white/[0.04] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]",
"relative flex min-h-[620px] scroll-mt-24 flex-col rounded-[28px] border border-white/12 bg-white/[0.04] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]",
isDetailReplacementPreset ? "overflow-visible" : "overflow-hidden"
)}
initial={{ opacity: 0, scale: 0.98 }}
@ -9860,84 +9886,8 @@ export function AgentGenerationWorkspaceDarkV2({
{isDetailReplacementPreset &&
!isDetailAiGenerationMethod &&
activeResultModeType === currentResultModeType ? (
<div className="grid flex-1 min-h-0 grid-cols-1 gap-0 xl:grid-cols-[320px_repeat(2,minmax(0,1fr))]">
<div className="border-b border-white/10 p-4 xl:border-b-0 xl:border-r">
<div className="mb-3 flex items-center justify-between">
<div>
<div className="text-sm font-semibold text-white">输入图</div>
<div className="text-xs text-white/40">
选中的白底图会作为参考图替换的输入图组
</div>
</div>
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/55">
{selectedProductAttachments.length} 张
</div>
</div>
<div className="space-y-3">
{selectedProductAttachments.length > 0 ? (
selectedProductAttachments.map((attachment, index) => (
<div
key={`${attachment?.url || "input"}-${index}`}
style={AGENT_DEFERRED_LIST_CARD_STYLE}
className="overflow-hidden rounded-[22px] border border-white/10 bg-black/20"
>
<div className="aspect-square overflow-hidden bg-black/30">
<AgentPreviewImage
src={attachment!.url}
alt={attachment!.name || `输入图 ${index + 1}`}
className="h-full w-full object-cover"
previewWidth={400}
previewQuality={74}
onError={(event) =>
handlePreviewImageLoadError(event, attachment!.url)
}
/>
</div>
<div className="border-t border-white/10 px-3 py-2">
<div className="text-xs font-medium text-white/80">
输入图 {index + 1}
</div>
<div className="truncate text-[11px] text-white/40">
{attachment?.name || "已选择白底图"}
</div>
</div>
</div>
))
) : (
<div className="flex min-h-[160px] flex-col items-center justify-center rounded-[22px] border border-dashed border-white/12 bg-black/20 px-4 text-center text-sm text-white/42">
请先在当前“选图与生成”页的输入图槽位中确认用于参考图替换的白底输入图。
</div>
)}
{selectedModelAttachment ? (
<div className="overflow-hidden rounded-[22px] border border-emerald-400/20 bg-emerald-500/[0.06]">
<div className="relative aspect-[5/4] overflow-hidden bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.12),transparent_58%),linear-gradient(180deg,#08110d_0%,#101916_100%)]">
<AgentPreviewImage
src={selectedModelAttachment.url}
alt={selectedModelAttachment.name || "模特图"}
className="h-full w-full object-cover"
previewWidth={720}
previewQuality={76}
onError={(event) =>
handlePreviewImageLoadError(event, selectedModelAttachment.url)
}
/>
<div className="absolute left-3 top-3 rounded-full border border-emerald-300/25 bg-black/45 px-2.5 py-1 text-[11px] text-emerald-100">
模特图
</div>
</div>
<div className="border-t border-emerald-400/15 px-3 py-2">
<div className="truncate text-xs font-medium text-emerald-100">
{selectedModelAttachment.name || "已选择模特图"}
</div>
</div>
</div>
) : null}
</div>
</div>
<div className="min-h-0 border-b border-white/10 p-4 xl:border-b-0 xl:border-r">
<div className="grid min-h-0 flex-1 grid-cols-1 gap-0 lg:grid-cols-2">
<div className="min-h-0 border-b border-white/10 p-4 lg:border-b-0 lg:border-r">
<div className="mb-3 flex items-center justify-between">
<div>
<div className="text-sm font-semibold text-white">{detailReferenceSourceLabel}参考图</div>
@ -10116,7 +10066,10 @@ export function AgentGenerationWorkspaceDarkV2({
</div>
</div>
<div className="min-h-0 p-4">
<div
ref={replacementResultsContentRef}
className="min-h-0 scroll-mt-24 p-4"
>
<div className="mb-3 flex items-center justify-between">
<div>
<div className="text-sm font-semibold text-white">替换结果</div>
@ -10777,7 +10730,10 @@ export function AgentGenerationWorkspaceDarkV2({
</div>
</div>
<div className="min-h-0 p-4">
<div
ref={replacementResultsContentRef}
className="min-h-0 scroll-mt-24 p-4"
>
<div className="mb-3 flex items-center justify-between">
<div>
<div className="text-sm font-semibold text-white">替换结果</div>

View File

@ -0,0 +1,36 @@
import type { AgentGenerationResultModeType } from "@/types/agent";
export function isReplacementResultMode(
modeType: AgentGenerationResultModeType | null | undefined
) {
return modeType === "detail_replacement" || modeType === "sku_replacement";
}
export function shouldShowAgentGridResultSection(params: {
activeModeType: AgentGenerationResultModeType;
activeResultCount: number;
currentModeType: AgentGenerationResultModeType;
pendingModeType?: AgentGenerationResultModeType | null;
transportIsGenerating: boolean;
hasDetailGenerationPlaceholderState: boolean;
}) {
if (params.activeResultCount > 0) {
return true;
}
if (params.pendingModeType === params.activeModeType) {
return true;
}
if (
params.transportIsGenerating &&
params.activeModeType === params.currentModeType
) {
return true;
}
return (
params.activeModeType === "detail_generation" &&
params.hasDetailGenerationPlaceholderState
);
}

View File

@ -0,0 +1,67 @@
import {
isReplacementResultMode,
shouldShowAgentGridResultSection,
} from "@/lib/agents/generation-result-visibility";
function expect(condition: boolean, message: string) {
if (!condition) {
throw new Error(message);
}
}
expect(
shouldShowAgentGridResultSection({
activeModeType: "detail_replacement",
activeResultCount: 0,
currentModeType: "detail_replacement",
pendingModeType: "detail_replacement",
transportIsGenerating: false,
hasDetailGenerationPlaceholderState: false,
}),
"detail replacement must show its result area before the first image returns"
);
expect(
shouldShowAgentGridResultSection({
activeModeType: "sku_replacement",
activeResultCount: 0,
currentModeType: "sku_replacement",
pendingModeType: "sku_replacement",
transportIsGenerating: false,
hasDetailGenerationPlaceholderState: false,
}),
"SKU replacement must show its result area before the first image returns"
);
expect(
shouldShowAgentGridResultSection({
activeModeType: "scene_generation",
activeResultCount: 0,
currentModeType: "scene_generation",
pendingModeType: null,
transportIsGenerating: true,
hasDetailGenerationPlaceholderState: false,
}),
"the active transport must reveal the current result area"
);
expect(
!shouldShowAgentGridResultSection({
activeModeType: "detail_replacement",
activeResultCount: 0,
currentModeType: "scene_generation",
pendingModeType: null,
transportIsGenerating: false,
hasDetailGenerationPlaceholderState: false,
}),
"an inactive empty result category must remain empty"
);
expect(
isReplacementResultMode("detail_replacement") &&
isReplacementResultMode("sku_replacement") &&
!isReplacementResultMode("detail_generation"),
"only replacement batches should trigger automatic result reveal"
);
console.log("agent generation result visibility tests passed");