fix: auto-fill brand model in agent workflow
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 20:21:31 +08:00
parent de947dd5aa
commit f082c20141
2 changed files with 49 additions and 144 deletions

View File

@ -90,6 +90,7 @@ import {
createEmptyAgentWorkflowMaterialSelections,
normalizeAgentWorkflowMaterialSelections,
} from "@/lib/agents/workflow-material-config";
import { normalizeAgentWorkflowBrandModelConfig } from "@/lib/agents/workflow-brand-model-config";
import {
buildAgentWorkflowComposerContext,
getAgentWorkflowProgress,
@ -274,6 +275,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
realtimeCanvasEnabled?: boolean;
realtimeCanvasServiceEnabled?: boolean;
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
agentWorkflowBrandModelConfig?: unknown;
agentWorkflowUiConfig?: {
steps?: Array<{
key: AgentWorkflowStepKey;
@ -300,10 +302,16 @@ export default function CanvasEditorPage({ params }: PageProps) {
const [aspectRatio, setAspectRatio] = useState("auto");
const [imageCount, setImageCount] = useState(1);
const previousNodeIdsRef = useRef<Set<string>>(new Set());
const previousBrandForModelRef = useRef("");
const autoSelectedBrandModelBrandRef = useRef("");
const requestedAgentMode = mode === "agent";
const isAgentFeatureEnabled = siteSettingsData?.agentEnabled !== false;
const isAgentMode =
isAgentFeatureEnabled && requestedAgentMode;
const resolvedBrandModelConfig = useMemo(
() => normalizeAgentWorkflowBrandModelConfig(siteSettingsData?.agentWorkflowBrandModelConfig),
[siteSettingsData?.agentWorkflowBrandModelConfig]
);
// 获取选中的图片节点
const selectedImageNode = selectedNodeIds.length === 1
@ -903,16 +911,51 @@ export default function CanvasEditorPage({ params }: PageProps) {
}, []);
useEffect(() => {
if (!selectedBrandModel) return;
const normalizedBrand = workflowInputs.brand.trim().toLowerCase();
if (!normalizedBrand) {
previousBrandForModelRef.current = "";
autoSelectedBrandModelBrandRef.current = "";
if (selectedBrandModel) {
setSelectedBrandModel(null);
}
return;
}
const brandChanged = previousBrandForModelRef.current !== normalizedBrand;
if (brandChanged) {
previousBrandForModelRef.current = normalizedBrand;
autoSelectedBrandModelBrandRef.current = "";
}
if (selectedBrandModel && selectedBrandModel.brandName.toLowerCase() !== normalizedBrand) {
setSelectedBrandModel(null);
return;
}
if (selectedBrandModel.brandName.toLowerCase() !== normalizedBrand) {
setSelectedBrandModel(null);
if (selectedBrandModel) {
return;
}
}, [selectedBrandModel, workflowInputs.brand]);
if (autoSelectedBrandModelBrandRef.current === normalizedBrand) {
return;
}
const matchedEntry =
resolvedBrandModelConfig.entries.find(
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
) || null;
const defaultBrandModel = matchedEntry?.images[0] || null;
if (!defaultBrandModel) {
return;
}
autoSelectedBrandModelBrandRef.current = normalizedBrand;
setSelectedBrandModel({
...defaultBrandModel,
brandName: matchedEntry?.brandName || workflowInputs.brand.trim(),
});
}, [resolvedBrandModelConfig.entries, selectedBrandModel, workflowInputs.brand]);
const persistCanvasState = useCallback(
async (reason: "autosave" | "delete" | "beforeunload") => {

View File

@ -411,7 +411,6 @@ export function AgentGenerationWorkspaceDarkV2({
: null
)
);
const [activeAssignmentSlotIndex, setActiveAssignmentSlotIndex] = useState<number | null>(null);
const [slotUploading, setSlotUploading] = useState<number | null>(null);
const [selectedModelId, setSelectedModelId] = useState<string>(
normalizedIncomingWorkspaceState.selectedModelId || "auto"
@ -534,32 +533,6 @@ export function AgentGenerationWorkspaceDarkV2({
);
const selectedModelAttachment =
modelSlotIndex >= 0 && attachments[modelSlotIndex] ? attachments[modelSlotIndex] : null;
const slotSourceLibrary = useMemo(
() =>
referenceImages.map((image, index) => ({
id: `${image.source}-${image.id || index}`,
source: image.source,
sourceLabel: image.sourceLabel,
label: image.label,
name: image.name,
url: image.url,
assetId: image.assetId || undefined,
mimeType: image.mimeType || undefined,
})),
[referenceImages]
);
const activeAssignmentIsModelSlot =
activeAssignmentSlotIndex !== null &&
modelSlotIndex >= 0 &&
activeAssignmentSlotIndex === modelSlotIndex;
const assignableSourceLibrary = useMemo(() => {
if (!slotSourceLibrary.length) return [];
if (activeAssignmentIsModelSlot) {
const brandModelOnly = slotSourceLibrary.filter((item) => item.source === "brandModel");
return brandModelOnly;
}
return slotSourceLibrary.filter((item) => item.source !== "brandModel");
}, [activeAssignmentIsModelSlot, slotSourceLibrary]);
const resolvedAgentModeState = agentModeState || localAgentModeState;
const emitAgentModeState = onAgentModeStateChange || setLocalAgentModeState;
const detailReplacementState = resolvedAgentModeState.detailReplacement;
@ -1104,19 +1077,6 @@ export function AgentGenerationWorkspaceDarkV2({
}
}, [latestGeneratedGroup?.imageUrls, selectedResultUrl]);
useEffect(() => {
if (agentUploadSlots.length === 0) {
setActiveAssignmentSlotIndex(null);
return;
}
setActiveAssignmentSlotIndex((current) => {
if (current !== null && current >= 0 && current < agentUploadSlots.length) {
return current;
}
return 0;
});
}, [agentUploadSlots.length]);
useEffect(() => {
if (!selectedAgentPresetId) {
setActiveToolStep("preset");
@ -1233,18 +1193,6 @@ export function AgentGenerationWorkspaceDarkV2({
},
[agentUploadSlots, updateAttachmentAtSlot]
);
const handleAssignSourceToSlot = useCallback(
(slotIndex: number, source: (typeof slotSourceLibrary)[number]) => {
updateAttachmentAtSlot(slotIndex, {
url: source.url,
name: source.name || source.label || `槽位${slotIndex + 1}`,
contentType: source.mimeType || "image/png",
assetId: source.assetId,
});
toast.success(`${source.label || source.name || "已选素材"} 已填入 ${agentUploadSlots[slotIndex]?.label || `槽位${slotIndex + 1}`}`);
},
[agentUploadSlots, slotSourceLibrary, updateAttachmentAtSlot]
);
const handleBatchDownloadImages = useCallback(async (params: {
imageUrls: string[];
zipName: string;
@ -1668,13 +1616,12 @@ export function AgentGenerationWorkspaceDarkV2({
输入图槽位
</h3>
<div className="mb-4 rounded-[20px] border border-emerald-400/15 bg-emerald-500/[0.06] px-4 py-3 text-xs leading-6 text-emerald-100/80">
上一步已选的白底图会自动带入对应产品槽位,这里只作为“检查 / 替换 / 微调映射”的调整区,不再单独占一个步骤页。
上一步已选的白底图和品牌模特图会自动带入对应槽位,这里只保留最终检查与单张上传替换,不再重复展示已选素材列表。
</div>
{agentUploadSlots.length > 0 ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{agentUploadSlots.map((slot, index) => {
const isActiveAssignmentSlot = activeAssignmentSlotIndex === index;
return (
<motion.div
key={slot.label}
@ -1683,8 +1630,7 @@ export function AgentGenerationWorkspaceDarkV2({
"group relative aspect-square overflow-hidden rounded-[24px] transition-all",
attachments[index]
? "ring-2 ring-emerald-400/45"
: "border-2 border-dashed border-white/15 bg-black/25 hover:border-emerald-400/35",
isActiveAssignmentSlot && "ring-2 ring-sky-400/65 ring-offset-2 ring-offset-black"
: "border-2 border-dashed border-white/15 bg-black/25 hover:border-emerald-400/35"
)}
>
<input
@ -1735,95 +1681,11 @@ export function AgentGenerationWorkspaceDarkV2({
>
{attachments[index] ? "更换" : "上传"}
</button>
<button
type="button"
onClick={() => setActiveAssignmentSlotIndex(index)}
className={cn(
"rounded-full border px-2.5 py-1 text-[11px] transition-colors",
isActiveAssignmentSlot
? "border-sky-400/45 bg-sky-500/[0.18] text-sky-100"
: "border-white/10 bg-black/45 text-white/70 hover:border-white/20 hover:text-white"
)}
>
{isActiveAssignmentSlot ? "当前目标" : "设为目标"}
</button>
</div>
</motion.div>
);
})}
</div>
{slotSourceLibrary.length > 0 ? (
<div className="rounded-[24px] border border-white/10 bg-black/20 p-3">
<div className="mb-3 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<div className="text-sm font-medium text-white">从已选素材快速替换槽位</div>
<div className="text-xs text-white/42">
先点击上方槽位的“设为目标”,再点击下方素材图,将它放入对应槽位。
</div>
</div>
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/60">
当前目标:
{activeAssignmentSlotIndex !== null
? agentUploadSlots[activeAssignmentSlotIndex]?.label || "未选择"
: "未选择"}
</div>
</div>
{assignableSourceLibrary.length > 0 ? (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{assignableSourceLibrary.map((source) => {
const assignedToActiveSlot =
activeAssignmentSlotIndex !== null &&
attachments[activeAssignmentSlotIndex]?.url === source.url;
return (
<button
key={source.id}
type="button"
onClick={() => {
if (activeAssignmentSlotIndex === null) return;
handleAssignSourceToSlot(activeAssignmentSlotIndex, source);
}}
disabled={activeAssignmentSlotIndex === null}
className={cn(
"overflow-hidden rounded-[20px] border text-left transition-all",
assignedToActiveSlot
? "border-emerald-400/55 bg-emerald-500/[0.08]"
: "border-white/10 bg-white/[0.03] hover:border-white/18",
activeAssignmentSlotIndex === null && "cursor-not-allowed opacity-45"
)}
>
<div className="relative aspect-square overflow-hidden bg-black/30">
<img
src={buildPreviewImageUrl(source.url, 320)}
alt={source.label}
className="h-full w-full object-cover"
/>
<div className="absolute left-2 top-2 rounded-full bg-black/60 px-2 py-1 text-[10px] text-white">
{source.sourceLabel}
</div>
</div>
<div className="space-y-1 border-t border-white/10 px-3 py-2">
<div className="truncate text-xs font-medium text-white/88">
{source.label}
</div>
<div className="truncate text-[11px] text-white/42">
{source.name}
</div>
</div>
</button>
);
})}
</div>
) : (
<div className="flex min-h-[92px] items-center justify-center rounded-[20px] border border-dashed border-white/10 bg-white/[0.02] px-4 text-center text-xs text-white/40">
{activeAssignmentIsModelSlot
? "当前模特槽位还没有可分配的品牌模特图,请先在“品牌模特”步骤里选择。"
: "当前产品槽位还没有来自已选素材区的图片,请先在当前页上方完成 SKU / SPU 选图。"}
</div>
)}
</div>
) : null}
</div>
) : (
<div className="flex min-h-[132px] flex-col items-center justify-center rounded-[24px] border border-dashed border-white/12 bg-black/20 px-4 text-center text-sm text-white/45">