diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 8a034a140..052819aad 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -886,6 +886,97 @@ export function AgentGenerationWorkspaceDarkV2({ (image) => image.source === "primary" || image.source === "secondary" ); const hasSatisfiedSlotRequirement = !hasAgentUploadSlots || attachmentCount >= requiredUploadCount; + const workflowSelectionCount = workflowContext?.selections?.length || 0; + const replacementReferenceSummary = isDetailReplacementPreset + ? `${detailReplacementReferenceCards.length} 张详情图` + : isSkuReplacementPreset + ? `${skuReplacementReferenceCards.length} 张 SKU 图` + : "当前模式不需要"; + const taskStatus = !selectedAgentPresetId + ? { + label: "待选模式", + className: "border-white/10 bg-white/[0.06] text-white/72", + } + : hasAgentUploadSlots && !hasSatisfiedSlotRequirement + ? { + label: "待补图", + className: "border-amber-400/20 bg-amber-500/[0.08] text-amber-100", + } + : isDetailReplacementPreset && detailReplacementReferenceCards.length === 0 + ? { + label: "待载入详情图", + className: "border-amber-400/20 bg-amber-500/[0.08] text-amber-100", + } + : isSkuReplacementPreset && skuReplacementReferenceCards.length === 0 + ? { + label: "待载入SKU图", + className: "border-amber-400/20 bg-amber-500/[0.08] text-amber-100", + } + : { + label: "可开始生成", + className: "border-emerald-400/20 bg-emerald-500/[0.08] text-emerald-100", + }; + const taskSummaryItems = [ + { + label: "当前模式", + value: selectedAgentPreset?.name || "未选择", + }, + { + label: "品牌 / 店铺", + value: + workflowContext?.brand || workflowContext?.store + ? `${workflowContext?.brand || "未选品牌"} / ${workflowContext?.store || "未选店铺"}` + : "未选择", + }, + { + label: "产品图", + value: + hasAgentUploadSlots && selectedAgentPreset?.type === "image" + ? `${selectedProductAttachments.length}/${productSlotCount} 张` + : "当前模式无需", + }, + { + label: "模特图", + value: modelSlotEnabled ? (selectedModelAttachment ? "已带入" : "待补充") : "当前模式无需", + }, + { + label: "风格卡片", + value: workflowSelectionCount > 0 ? `${workflowSelectionCount} 项已选` : "待选择", + }, + { + label: "参考图", + value: replacementReferenceSummary, + }, + ]; + const taskDirective = !selectedAgentPresetId + ? { + title: "先选择一个生图模式", + description: "模式会决定后续的输入槽位、参考图区和结果执行方式。", + } + : hasMaterialStep && hasAgentUploadSlots && !hasSatisfiedSlotRequirement + ? { + title: `先补齐剩余 ${missingAttachmentCount} 张输入图`, + description: "输入图没有准备完成前,系统不会开放生成入口,避免用户走到后面才返工。", + } + : hasWorkflowStyleStep && workflowSelectionCount === 0 + ? { + title: "继续选择风格步骤卡片", + description: "把风格卡片补齐后,组合 Prompt 会更完整,结果风格也会更稳定。", + } + : isDetailReplacementPreset && detailReplacementReferenceCards.length === 0 + ? { + title: "先载入详情参考图", + description: "详情替换需要一整套详情参考图,右侧结果会按相同顺序逐屏生成。", + } + : isSkuReplacementPreset && skuReplacementReferenceCards.length === 0 + ? { + title: "先载入 SKU 参考图", + description: "SKU 替换需要先确定一组参考图,系统才会执行一对一替换。", + } + : { + title: "当前已经可以开始生成", + description: "现在只需要检查补充要求和生成参数,就可以直接开始执行。", + }; const slotRequirementMessage = hasAgentUploadSlots && !hasSatisfiedSlotRequirement ? `当前还缺少 ${missingAttachmentCount} 张输入图,请先在“选择 SPU / SKU”步骤补齐后再继续。` @@ -1455,6 +1546,53 @@ export function AgentGenerationWorkspaceDarkV2({ } target.scrollIntoView({ behavior: "smooth", block: "start" }); }, [hasAgentUploadSlots, hasMaterialStep, hasSatisfiedSlotRequirement, slotRequirementMessage]); + const taskSummaryAction = useMemo(() => { + if (!selectedAgentPresetId) { + return { + label: "去选模式", + sectionId: "agent-anchor-mode", + }; + } + if (hasMaterialStep && hasAgentUploadSlots && !hasSatisfiedSlotRequirement) { + return { + label: "去补图", + sectionId: "agent-anchor-materials", + }; + } + if (hasWorkflowStyleStep && workflowSelectionCount === 0) { + return { + label: "去选风格", + sectionId: "agent-anchor-style", + }; + } + if (isDetailReplacementPreset && detailReplacementReferenceCards.length === 0) { + return { + label: "去查详情图", + sectionId: "agent-anchor-results", + }; + } + if (isSkuReplacementPreset && skuReplacementReferenceCards.length === 0) { + return { + label: "去查SKU参考图", + sectionId: "agent-anchor-results", + }; + } + return { + label: "前往生成区", + sectionId: "agent-anchor-generate", + }; + }, [ + detailReplacementReferenceCards.length, + hasAgentUploadSlots, + hasMaterialStep, + hasSatisfiedSlotRequirement, + hasWorkflowStyleStep, + isDetailReplacementPreset, + isSkuReplacementPreset, + selectedAgentPresetId, + skuReplacementReferenceCards.length, + workflowSelectionCount, + ]); const updateDetailReplacementState = useCallback( ( updater: @@ -1897,6 +2035,59 @@ export function AgentGenerationWorkspaceDarkV2({ +
+
+
+
+ + 下一步建议 + + + {taskStatus.label} + +
+
+
{taskDirective.title}
+
+ {taskDirective.description} +
+
+
+ {taskSummaryItems.map((item) => ( +
+
{item.label}
+
+ {item.value} +
+
+ ))} +
+
+ +
+
+ {slotRequirementMessage || taskDirective.description} +
+ +
+
+
+
diff --git a/components/agents/AgentPresetCardDark.tsx b/components/agents/AgentPresetCardDark.tsx index 6b70bad97..14f9693eb 100644 --- a/components/agents/AgentPresetCardDark.tsx +++ b/components/agents/AgentPresetCardDark.tsx @@ -3,7 +3,10 @@ import { motion } from "framer-motion"; import { Play, Copy, Pencil, Sparkles, Trash2, Video, Wand2, FileText, Clock, User } from "lucide-react"; import { cn } from "@/lib/utils"; -import { getAgentPresetModeTypeFromDefaultParams } from "@/lib/agents/preset-mode-config"; +import { + getAgentPresetModeTypeFromDefaultParams, + normalizeAgentPresetModeConfig, +} from "@/lib/agents/preset-mode-config"; import type { AgentPreset } from "@/types/agent"; interface AgentPresetCardDarkProps { @@ -75,6 +78,7 @@ export function AgentPresetCardDark({ const typeInfo = typeConfig[preset.type] || typeConfig.image; const TypeIcon = typeInfo.icon; const imageModeType = getAgentPresetModeTypeFromDefaultParams(preset.defaultParams); + const modeConfig = normalizeAgentPresetModeConfig(preset.defaultParams); const imageModeLabel = imageModeType === "detail_replacement" ? "详情替换" @@ -83,6 +87,36 @@ export function AgentPresetCardDark({ : imageModeType === "scene_generation" ? "场景图" : null; + const requiresImageCount = + preset.type === "image" ? Math.max(0, preset.requiredUploadCount || preset.slotCount || 0) : 0; + const includesModelSlot = + preset.type === "image" + ? imageModeType === "scene_generation" + ? modeConfig.scene.includeModelSlot + : imageModeType === "detail_replacement" + ? modeConfig.detail.includeModelSlot + : imageModeType === "sku_replacement" + ? modeConfig.sku.includeModelSlot + : preset.requiredUploadCount > preset.slotCount + : false; + const modelControlLabel = + preset.type === "image" + ? imageModeType === "scene_generation" && modeConfig.scene.allowModelOverride + ? "可切模型" + : "锁定模型" + : null; + const categoryHint = + preset.categoryPath?.level3Name || + preset.categoryPath?.level2Name || + preset.categoryPath?.level1Name || + preset.categoryPath?.sectionName || + null; + const compactSummaryBadges = [ + imageModeLabel || typeInfo.label, + preset.type === "image" && requiresImageCount > 0 ? `需${requiresImageCount}张图` : null, + preset.type === "image" ? (includesModelSlot ? "含模特槽" : "无模特槽") : null, + modelControlLabel, + ].filter((item): item is string => Boolean(item)); const showEdit = Boolean(onEdit); const showDelete = Boolean(onDelete); const showFork = Boolean(onFork) && !showEdit && !showDelete; @@ -169,7 +203,32 @@ export function AgentPresetCardDark({
{preset.name}
-
+
+ {compactSummaryBadges.map((badge, badgeIndex) => ( + + {badge} + + ))} +
+
+
+ 默认模型:{preset.defaultModel?.displayName || "未配置"} +
+ {preset.description ? ( +
{preset.description}
+ ) : categoryHint ? ( +
适用分类:{categoryHint}
+ ) : null} +
+
贡献者:{ownerName} @@ -339,6 +398,22 @@ export function AgentPresetCardDark({

{preset.name}

+ +
+ {compactSummaryBadges.slice(0, 4).map((badge, badgeIndex) => ( + + {badge} + + ))} +
{/* 元信息 */}
@@ -347,12 +422,12 @@ export function AgentPresetCardDark({ {ownerName} - {preset.slotCount > 0 && ( + {preset.defaultModel?.displayName ? ( - {preset.slotCount}槽 + {preset.defaultModel.displayName} - )} + ) : null}
{publishedAt && ( diff --git a/components/agents/AgentWorkflowMaterialSelectionPanel.tsx b/components/agents/AgentWorkflowMaterialSelectionPanel.tsx index c069edf29..ab87a927f 100644 --- a/components/agents/AgentWorkflowMaterialSelectionPanel.tsx +++ b/components/agents/AgentWorkflowMaterialSelectionPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Plus, Sparkles } from "lucide-react"; import { AgentWorkflowMaterialPicker } from "@/components/agents/AgentWorkflowMaterialPicker"; import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config"; @@ -101,6 +101,84 @@ export function AgentWorkflowMaterialSelectionPanel({ typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0 ? Math.max(productSelectionLimit - primarySelectedCount, secondarySelectedCount, 0) : null; + const primarySelectedImages = useMemo( + () => + (materials.primary.result?.images || []).filter((image) => + materials.primary.selectedImageIds.includes(image.id) + ), + [materials.primary.result?.images, materials.primary.selectedImageIds] + ); + const secondarySelectedImages = useMemo( + () => + (materials.secondary.result?.images || []).filter((image) => + materials.secondary.selectedImageIds.includes(image.id) + ), + [materials.secondary.result?.images, materials.secondary.selectedImageIds] + ); + const slotPreviewCount = useMemo(() => { + if ( + typeof productSelectionLimit === "number" && + Number.isFinite(productSelectionLimit) && + productSelectionLimit > 0 + ) { + return Math.round(productSelectionLimit); + } + return primarySelectedImages.length + secondarySelectedImages.length; + }, [primarySelectedImages.length, productSelectionLimit, secondarySelectedImages.length]); + const slotPreviewItems = useMemo(() => { + const orderedSources = [ + ...primarySelectedImages.map((image, index) => ({ + source: "主商品" as const, + image, + sourceIndex: index, + })), + ...secondarySelectedImages.map((image, index) => ({ + source: "搭配商品" as const, + image, + sourceIndex: index, + })), + ]; + + return Array.from({ length: slotPreviewCount }, (_, index) => { + const assigned = orderedSources[index] || null; + return { + slotLabel: `产品图${index + 1}`, + assigned, + }; + }); + }, [primarySelectedImages, secondarySelectedImages, slotPreviewCount]); + const hasAnyQueryInput = + Boolean(String(inputs.primarySku || "").trim()) || + Boolean(String(inputs.secondarySku || "").trim()); + const hasSkuSelectionPending = + Boolean(materials.primary.result?.skuOptions?.length) || + Boolean(materials.secondary.result?.skuOptions?.length); + const hasAnyMaterialResult = + Boolean(materials.primary.result?.images?.length) || + Boolean(materials.secondary.result?.images?.length) || + hasSkuSelectionPending; + const hasAnySelectedImages = + materials.primary.selectedImageIds.length > 0 || materials.secondary.selectedImageIds.length > 0; + const materialGuideSteps = [ + { + badge: "01", + label: "输入型号", + description: "先选择按 SPU 或 SKU 搜索,再输入商品型号。", + status: hasAnyQueryInput ? "completed" : "current", + }, + { + badge: "02", + label: "选择SKU", + description: "如果是 SPU 查询,先从返回的 SKU 列表里点选具体 SKU。", + status: hasSkuSelectionPending ? "current" : hasAnyMaterialResult ? "completed" : "pending", + }, + { + badge: "03", + label: "勾选图片", + description: "勾选你要带入生图的白底图,系统会自动映射到产品槽位。", + status: hasAnySelectedImages ? "completed" : hasAnyMaterialResult ? "current" : "pending", + }, + ] as const; useEffect(() => { if (hasSecondaryMaterialContent) { @@ -222,6 +300,157 @@ export function AgentWorkflowMaterialSelectionPanel({
) : null} +
+
+
+ 选图流程 +
+
+ 按这 3 步操作就能完成输入图准备,避免来回试错。 +
+
+ +
+ {materialGuideSteps.map((step) => ( +
+
+ + {step.badge} + +
+ {step.label} +
+
+
+ {step.description} +
+
+ ))} +
+
+ + {slotPreviewItems.length > 0 ? ( +
+
+
+ 产品图槽位映射 +
+
+ 当前已选图片会按顺序自动带入后续生图槽位,先主商品,后搭配商品。 +
+
+ +
+ {slotPreviewItems.map((item, index) => { + const source = item.assigned?.source || null; + const previewUrl = item.assigned?.image.imageUrl || ""; + const isAssigned = Boolean(item.assigned && previewUrl); + + return ( +
+
+ {isAssigned ? ( + {item.assigned?.image.label + ) : ( +
+ 待选择 +
+ )} +
+ {item.slotLabel} +
+
+ {source || "空槽位"} +
+
+
+
+ {isAssigned + ? item.assigned?.image.label || `已映射到${item.slotLabel}` + : `${item.slotLabel}待补图`} +
+
+ {isAssigned + ? `${source}第 ${(item.assigned?.sourceIndex || 0) + 1} 张` + : "先在下方搜索并选择图片"} +
+
+
+ ); + })} +
+
+ ) : null} + option.buttonName).join(" / "); +} + function StepOptionCard({ option, step, @@ -303,6 +319,16 @@ export function AgentWorkflowPanelDarkV2({ () => getAgentWorkflowProgress({ config: resolvedConfig, selections }), [resolvedConfig, selections] ); + const stepSelectionSummaryMap = useMemo(() => { + const entries = flattenAgentWorkflowSteps(resolvedConfig.steps).map(({ step }) => [ + step.key, + getWorkflowStepSelectionSummary({ + step, + selectionValue: selections[step.key], + }), + ]); + return Object.fromEntries(entries) as Record; + }, [resolvedConfig.steps, selections]); const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState(""); const [pendingAutoAdvanceStepKey, setPendingAutoAdvanceStepKey] = useState(null); const activeEmbeddedStepIndex = Math.max( @@ -497,6 +523,7 @@ export function AgentWorkflowPanelDarkV2({ selectionValue: selections[step.key], }); const active = step.key === activeEmbeddedStepEntry.step.key; + const stepSummary = stepSelectionSummaryMap[step.key] || ""; return ( ); })} @@ -545,6 +586,11 @@ export function AgentWorkflowPanelDarkV2({ ) : null}
+
+ {stepSelectionSummaryMap[activeEmbeddedStepEntry.step.key] + ? `已选:${stepSelectionSummaryMap[activeEmbeddedStepEntry.step.key]}` + : "待选择:完成当前卡片后会自动推进到下一步"} +
{activeEmbeddedStepEntry.step.description ? (

{activeEmbeddedStepEntry.step.description} @@ -582,6 +628,11 @@ export function AgentWorkflowPanelDarkV2({ ) : null} +

+ {stepSelectionSummaryMap[step.key] + ? `已选:${stepSelectionSummaryMap[step.key]}` + : "待选择:先确定这一组风格卡片,系统再继续组合 Prompt"} +
{step.description && (

{step.description}

)}