fix: refine agent workflow step navigation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-21 12:32:57 +08:00
parent 9631c10812
commit 21a915cb13
3 changed files with 160 additions and 99 deletions

View File

@ -1750,14 +1750,7 @@ export function AgentGenerationWorkspaceDarkV2({
transition={{ delay: 0.06 }}
className="rounded-[28px] border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
>
<div className="mb-5 border-b border-white/10 pb-5">
<div className="mb-4 space-y-1">
<div className="text-xs uppercase tracking-[0.2em] text-white/35">Workflow Anchors</div>
<h3 className="text-lg font-semibold text-white">单页生图流程</h3>
<p className="text-sm text-white/45">
在同一页面完成生图模式选择、SPU / SKU 选图、风格步骤配置、生成和结果预览。
</p>
</div>
<div className="mb-5 border-b border-white/10 pb-4">
<div className="agent-dark-scrollbar flex gap-2 overflow-x-auto pb-1">
{unifiedAnchorSteps.map((step) => (
<button

View File

@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Sparkles } from "lucide-react";
import { useEffect, useState } from "react";
import { Plus, Sparkles } from "lucide-react";
import { AgentWorkflowMaterialPicker } from "@/components/agents/AgentWorkflowMaterialPicker";
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
import { cn } from "@/lib/utils";
@ -29,12 +29,17 @@ export function AgentWorkflowMaterialSelectionPanel({
onInputsChange,
materials,
onMaterialsChange,
title = "SKU / SPU 选图",
description = "先确定生图模式,再搜索主 SKU 或搭配 SKU 的白底图。选中的图片会自动进入对应槽位。",
title,
description,
disabled = false,
disabledHint = "请先在下方选择一个生图模式,再启用 SKU / SPU 搜图。",
theme = "light",
}: AgentWorkflowMaterialSelectionPanelProps) {
const hasSecondaryMaterialContent =
Boolean(String(inputs.secondarySku || "").trim()) ||
Boolean(materials.secondary.result) ||
materials.secondary.selectedImageIds.length > 0;
const buildLoadingMaterialResult = (params: {
keyword: string;
currentResult?: AgentWorkflowMaterialSearchResult | null;
@ -81,6 +86,15 @@ export function AgentWorkflowMaterialSelectionPanel({
primary: "sku",
secondary: "sku",
});
const [isSecondaryMaterialVisible, setIsSecondaryMaterialVisible] = useState(
hasSecondaryMaterialContent
);
useEffect(() => {
if (hasSecondaryMaterialContent) {
setIsSecondaryMaterialVisible(true);
}
}, [hasSecondaryMaterialContent]);
const updateMaterialSearchState = (
source: "primary" | "secondary",
@ -170,25 +184,31 @@ export function AgentWorkflowMaterialSelectionPanel({
isDark ? "border-white/12 bg-white/[0.04]" : "border-[#dbe3ff] bg-[#f8faff]"
)}
>
<div className="space-y-1">
<div
className={cn(
"flex items-center gap-2 text-sm font-medium",
isDark ? "text-white/88" : "text-[#334155]"
)}
>
<Sparkles className={cn("h-4 w-4", isDark ? "text-emerald-300" : "text-[#4f46e5]")} />
<span>{title}</span>
{title || description ? (
<div className="space-y-1">
{title ? (
<div
className={cn(
"flex items-center gap-2 text-sm font-medium",
isDark ? "text-white/88" : "text-[#334155]"
)}
>
<Sparkles className={cn("h-4 w-4", isDark ? "text-emerald-300" : "text-[#4f46e5]")} />
<span>{title}</span>
</div>
) : null}
{description ? (
<div
className={cn(
"text-xs leading-5",
isDark ? "text-white/55" : "text-[#5b6472]"
)}
>
{description}
</div>
) : null}
</div>
<div
className={cn(
"text-xs leading-5",
isDark ? "text-white/55" : "text-[#5b6472]"
)}
>
{description}
</div>
</div>
) : null}
<AgentWorkflowMaterialPicker
title="主 SKU / SPU"
@ -259,74 +279,110 @@ export function AgentWorkflowMaterialSelectionPanel({
}
/>
<AgentWorkflowMaterialPicker
title="搭配 SKU"
description="搭配商品图会优先自动填充到“产品图2”;品牌模特图会在上方单独选择并进入模特图槽位。"
value={inputs.secondarySku}
queryMode={materialSearchMode.secondary}
placeholder={materialSearchMode.secondary === "spu" ? "输入搭配 SPU" : "输入搭配 SKU"}
buttonLabel={materialSearchMode.secondary === "spu" ? "查SKU列表" : "搜索搭配图"}
searchNote={
materialSearchMode.secondary === "spu"
? "按 SPU 搜索时会先返回该 SPU 下的 SKU 列表,再选具体 SKU 获取白底图。"
: "选中的搭配图会进入第二个产品槽位,你也可以在下面继续手动替换。"
}
disabled={disabled}
disabledHint={disabledHint}
isLoading={materialSearchState.secondary.isLoading}
errorText={materialSearchState.secondary.errorText}
result={materials.secondary.result}
selectedImageIds={materials.secondary.selectedImageIds}
onValueChange={(value) => handleMaterialInputChange("secondary", value)}
onQueryModeChange={(nextMode) =>
setMaterialSearchMode((prev) => ({
...prev,
secondary: nextMode,
}))
}
onSearch={() => {
void handleSearchMaterials("secondary");
}}
onSkuOptionSelect={(skuName) => {
const selectedOption = materials.secondary.result?.skuOptions?.find(
(option) => option.skuName === skuName
);
const placeholderCount = selectedOption?.whiteBackgroundCount || 8;
onInputsChange({
...inputs,
secondarySku: skuName,
});
onMaterialsChange({
...materials,
secondary: {
result: buildLoadingMaterialResult({
keyword: skuName,
currentResult: materials.secondary.result,
placeholderCount,
}),
selectedImageIds: [],
},
});
setMaterialSearchMode((prev) => ({
...prev,
secondary: "sku",
}));
void handleSearchMaterials("secondary", {
keyword: skuName,
queryType: "sku",
});
}}
theme={theme}
onSelectedImageIdsChange={(next) =>
onMaterialsChange({
...materials,
secondary: {
...materials.secondary,
selectedImageIds: next,
},
})
}
/>
{isSecondaryMaterialVisible ? (
<AgentWorkflowMaterialPicker
title="搭配 SKU"
description="搭配商品图会优先自动填充到“产品图2”;品牌模特图会在上方单独选择并进入模特图槽位。"
value={inputs.secondarySku}
queryMode={materialSearchMode.secondary}
placeholder={materialSearchMode.secondary === "spu" ? "输入搭配 SPU" : "输入搭配 SKU"}
buttonLabel={materialSearchMode.secondary === "spu" ? "查SKU列表" : "搜索搭配图"}
searchNote={
materialSearchMode.secondary === "spu"
? "按 SPU 搜索时会先返回该 SPU 下的 SKU 列表,再选具体 SKU 获取白底图。"
: "选中的搭配图会进入第二个产品槽位,你也可以在下面继续手动替换。"
}
disabled={disabled}
disabledHint={disabledHint}
isLoading={materialSearchState.secondary.isLoading}
errorText={materialSearchState.secondary.errorText}
result={materials.secondary.result}
selectedImageIds={materials.secondary.selectedImageIds}
onValueChange={(value) => handleMaterialInputChange("secondary", value)}
onQueryModeChange={(nextMode) =>
setMaterialSearchMode((prev) => ({
...prev,
secondary: nextMode,
}))
}
onSearch={() => {
void handleSearchMaterials("secondary");
}}
onSkuOptionSelect={(skuName) => {
const selectedOption = materials.secondary.result?.skuOptions?.find(
(option) => option.skuName === skuName
);
const placeholderCount = selectedOption?.whiteBackgroundCount || 8;
onInputsChange({
...inputs,
secondarySku: skuName,
});
onMaterialsChange({
...materials,
secondary: {
result: buildLoadingMaterialResult({
keyword: skuName,
currentResult: materials.secondary.result,
placeholderCount,
}),
selectedImageIds: [],
},
});
setMaterialSearchMode((prev) => ({
...prev,
secondary: "sku",
}));
void handleSearchMaterials("secondary", {
keyword: skuName,
queryType: "sku",
});
}}
theme={theme}
onSelectedImageIdsChange={(next) =>
onMaterialsChange({
...materials,
secondary: {
...materials.secondary,
selectedImageIds: next,
},
})
}
/>
) : (
<button
type="button"
onClick={() => setIsSecondaryMaterialVisible(true)}
disabled={disabled}
className={cn(
"flex w-full items-center justify-between rounded-[24px] border border-dashed px-4 py-3 text-left transition-colors",
disabled
? isDark
? "cursor-not-allowed border-white/10 bg-white/[0.02] text-white/30"
: "cursor-not-allowed border-[#dbe3ff] bg-[#f8faff] text-[#94a3b8]"
: isDark
? "border-white/14 bg-white/[0.03] text-white/78 hover:border-emerald-300/30 hover:bg-white/[0.05]"
: "border-[#c7d2fe] bg-white text-[#334155] hover:border-[#818cf8] hover:bg-[#f8faff]"
)}
>
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm font-medium">
<Plus className={cn("h-4 w-4", isDark ? "text-emerald-300" : "text-[#4f46e5]")} />
<span>添加搭配SKU</span>
</div>
<div className={cn("text-xs leading-5", isDark ? "text-white/45" : "text-[#64748b]")}>
需要补充第二个产品图时再展开这个模块,不使用时默认保持隐藏。
</div>
</div>
<span
className={cn(
"rounded-full px-2.5 py-1 text-[11px] font-medium",
isDark ? "bg-white/[0.06] text-white/72" : "bg-[#eef2ff] text-[#4f46e5]"
)}
>
可选
</span>
</button>
)}
</div>
);
}

View File

@ -235,6 +235,7 @@ export function AgentWorkflowPanelDarkV2({
[resolvedConfig, selections]
);
const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState<string>("");
const [pendingAutoAdvanceStepKey, setPendingAutoAdvanceStepKey] = useState<string | null>(null);
const activeEmbeddedStepIndex = Math.max(
flattenedResolvedSteps.findIndex(({ step }) => step.key === activeEmbeddedStepKey),
0
@ -269,6 +270,10 @@ export function AgentWorkflowPanelDarkV2({
return;
}
if (pendingAutoAdvanceStepKey !== activeEmbeddedStepEntry.step.key) {
return;
}
const currentStepComplete = isAgentWorkflowStepSelectionComplete({
step: activeEmbeddedStepEntry.step,
selectionValue: selections[activeEmbeddedStepEntry.step.key],
@ -290,12 +295,15 @@ export function AgentWorkflowPanelDarkV2({
if (nextStep && nextStep.step.key !== activeEmbeddedStepKey) {
setActiveEmbeddedStepKey(nextStep.step.key);
}
setPendingAutoAdvanceStepKey(null);
}, [
activeEmbeddedStepEntry,
activeEmbeddedStepIndex,
activeEmbeddedStepKey,
embedded,
flattenedResolvedSteps,
pendingAutoAdvanceStepKey,
selections,
]);
@ -341,6 +349,7 @@ export function AgentWorkflowPanelDarkV2({
const prefix = parentPath.slice(0, depth - 1);
const nextKeys =
currentSelectedKey === option.key ? prefix : [...prefix, option.key];
setPendingAutoAdvanceStepKey(step.key);
onSelectionsChange({
...selections,
[step.key]: nextKeys.length > 0 ? serializeAgentWorkflowSelectionPath(nextKeys) : undefined,
@ -451,7 +460,10 @@ export function AgentWorkflowPanelDarkV2({
<button
key={step.key}
type="button"
onClick={() => setActiveEmbeddedStepKey(step.key)}
onClick={() => {
setPendingAutoAdvanceStepKey(null);
setActiveEmbeddedStepKey(step.key);
}}
className={cn(
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-colors",
active