fix: auto-fill brand model in agent workflow
This commit is contained in:
parent
de947dd5aa
commit
f082c20141
@ -90,6 +90,7 @@ import {
|
|||||||
createEmptyAgentWorkflowMaterialSelections,
|
createEmptyAgentWorkflowMaterialSelections,
|
||||||
normalizeAgentWorkflowMaterialSelections,
|
normalizeAgentWorkflowMaterialSelections,
|
||||||
} from "@/lib/agents/workflow-material-config";
|
} from "@/lib/agents/workflow-material-config";
|
||||||
|
import { normalizeAgentWorkflowBrandModelConfig } from "@/lib/agents/workflow-brand-model-config";
|
||||||
import {
|
import {
|
||||||
buildAgentWorkflowComposerContext,
|
buildAgentWorkflowComposerContext,
|
||||||
getAgentWorkflowProgress,
|
getAgentWorkflowProgress,
|
||||||
@ -274,6 +275,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
realtimeCanvasEnabled?: boolean;
|
realtimeCanvasEnabled?: boolean;
|
||||||
realtimeCanvasServiceEnabled?: boolean;
|
realtimeCanvasServiceEnabled?: boolean;
|
||||||
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
|
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
|
||||||
|
agentWorkflowBrandModelConfig?: unknown;
|
||||||
agentWorkflowUiConfig?: {
|
agentWorkflowUiConfig?: {
|
||||||
steps?: Array<{
|
steps?: Array<{
|
||||||
key: AgentWorkflowStepKey;
|
key: AgentWorkflowStepKey;
|
||||||
@ -300,10 +302,16 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
const [aspectRatio, setAspectRatio] = useState("auto");
|
const [aspectRatio, setAspectRatio] = useState("auto");
|
||||||
const [imageCount, setImageCount] = useState(1);
|
const [imageCount, setImageCount] = useState(1);
|
||||||
const previousNodeIdsRef = useRef<Set<string>>(new Set());
|
const previousNodeIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
const previousBrandForModelRef = useRef("");
|
||||||
|
const autoSelectedBrandModelBrandRef = useRef("");
|
||||||
const requestedAgentMode = mode === "agent";
|
const requestedAgentMode = mode === "agent";
|
||||||
const isAgentFeatureEnabled = siteSettingsData?.agentEnabled !== false;
|
const isAgentFeatureEnabled = siteSettingsData?.agentEnabled !== false;
|
||||||
const isAgentMode =
|
const isAgentMode =
|
||||||
isAgentFeatureEnabled && requestedAgentMode;
|
isAgentFeatureEnabled && requestedAgentMode;
|
||||||
|
const resolvedBrandModelConfig = useMemo(
|
||||||
|
() => normalizeAgentWorkflowBrandModelConfig(siteSettingsData?.agentWorkflowBrandModelConfig),
|
||||||
|
[siteSettingsData?.agentWorkflowBrandModelConfig]
|
||||||
|
);
|
||||||
|
|
||||||
// 获取选中的图片节点
|
// 获取选中的图片节点
|
||||||
const selectedImageNode = selectedNodeIds.length === 1
|
const selectedImageNode = selectedNodeIds.length === 1
|
||||||
@ -903,16 +911,51 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedBrandModel) return;
|
|
||||||
const normalizedBrand = workflowInputs.brand.trim().toLowerCase();
|
const normalizedBrand = workflowInputs.brand.trim().toLowerCase();
|
||||||
if (!normalizedBrand) {
|
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);
|
setSelectedBrandModel(null);
|
||||||
return;
|
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(
|
const persistCanvasState = useCallback(
|
||||||
async (reason: "autosave" | "delete" | "beforeunload") => {
|
async (reason: "autosave" | "delete" | "beforeunload") => {
|
||||||
|
|||||||
@ -411,7 +411,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
: null
|
: null
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [activeAssignmentSlotIndex, setActiveAssignmentSlotIndex] = useState<number | null>(null);
|
|
||||||
const [slotUploading, setSlotUploading] = useState<number | null>(null);
|
const [slotUploading, setSlotUploading] = useState<number | null>(null);
|
||||||
const [selectedModelId, setSelectedModelId] = useState<string>(
|
const [selectedModelId, setSelectedModelId] = useState<string>(
|
||||||
normalizedIncomingWorkspaceState.selectedModelId || "auto"
|
normalizedIncomingWorkspaceState.selectedModelId || "auto"
|
||||||
@ -534,32 +533,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
);
|
);
|
||||||
const selectedModelAttachment =
|
const selectedModelAttachment =
|
||||||
modelSlotIndex >= 0 && attachments[modelSlotIndex] ? attachments[modelSlotIndex] : null;
|
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 resolvedAgentModeState = agentModeState || localAgentModeState;
|
||||||
const emitAgentModeState = onAgentModeStateChange || setLocalAgentModeState;
|
const emitAgentModeState = onAgentModeStateChange || setLocalAgentModeState;
|
||||||
const detailReplacementState = resolvedAgentModeState.detailReplacement;
|
const detailReplacementState = resolvedAgentModeState.detailReplacement;
|
||||||
@ -1104,19 +1077,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
}
|
}
|
||||||
}, [latestGeneratedGroup?.imageUrls, selectedResultUrl]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!selectedAgentPresetId) {
|
if (!selectedAgentPresetId) {
|
||||||
setActiveToolStep("preset");
|
setActiveToolStep("preset");
|
||||||
@ -1233,18 +1193,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
},
|
},
|
||||||
[agentUploadSlots, updateAttachmentAtSlot]
|
[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: {
|
const handleBatchDownloadImages = useCallback(async (params: {
|
||||||
imageUrls: string[];
|
imageUrls: string[];
|
||||||
zipName: string;
|
zipName: string;
|
||||||
@ -1668,13 +1616,12 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
输入图槽位
|
输入图槽位
|
||||||
</h3>
|
</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 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>
|
</div>
|
||||||
{agentUploadSlots.length > 0 ? (
|
{agentUploadSlots.length > 0 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
{agentUploadSlots.map((slot, index) => {
|
{agentUploadSlots.map((slot, index) => {
|
||||||
const isActiveAssignmentSlot = activeAssignmentSlotIndex === index;
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={slot.label}
|
key={slot.label}
|
||||||
@ -1683,8 +1630,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
"group relative aspect-square overflow-hidden rounded-[24px] transition-all",
|
"group relative aspect-square overflow-hidden rounded-[24px] transition-all",
|
||||||
attachments[index]
|
attachments[index]
|
||||||
? "ring-2 ring-emerald-400/45"
|
? "ring-2 ring-emerald-400/45"
|
||||||
: "border-2 border-dashed border-white/15 bg-black/25 hover:border-emerald-400/35",
|
: "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"
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@ -1735,95 +1681,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
>
|
>
|
||||||
{attachments[index] ? "更换" : "上传"}
|
{attachments[index] ? "更换" : "上传"}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</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>
|
||||||
) : (
|
) : (
|
||||||
<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">
|
<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