fix: auto-fill brand model in agent workflow
This commit is contained in:
parent
de947dd5aa
commit
f082c20141
@ -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") => {
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user