feat: improve agent workflow guidance
This commit is contained in:
parent
656aacbd01
commit
c0d4375daa
@ -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({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sticky top-[76px] z-20 mb-6 rounded-[24px] border border-white/10 bg-black/72 px-4 py-4 shadow-[0_12px_36px_rgba(0,0,0,0.22)] backdrop-blur-xl">
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.06] px-2.5 py-1 text-[11px] font-medium text-white/78">
|
||||
下一步建议
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full border px-2.5 py-1 text-[11px] font-medium",
|
||||
taskStatus.className
|
||||
)}
|
||||
>
|
||||
{taskStatus.label}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-base font-semibold text-white">{taskDirective.title}</div>
|
||||
<div className="mt-1 text-sm leading-6 text-white/45">
|
||||
{taskDirective.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 xl:grid-cols-6">
|
||||
{taskSummaryItems.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded-[18px] border border-white/10 bg-white/[0.03] px-3 py-2"
|
||||
>
|
||||
<div className="text-[11px] text-white/42">{item.label}</div>
|
||||
<div className="mt-1 truncate text-sm font-medium text-white/88">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="max-w-[320px] text-left text-xs leading-5 text-white/45 xl:text-right">
|
||||
{slotRequirementMessage || taskDirective.description}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => scrollToSection(taskSummaryAction.sectionId)}
|
||||
className="border-white/12 bg-white/[0.05] text-white/88 hover:bg-white/[0.10] hover:text-white"
|
||||
>
|
||||
{taskSummaryAction.label}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<section id="agent-anchor-mode" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="mb-4 flex flex-col gap-4 border-b border-white/10 pb-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
|
||||
@ -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({
|
||||
<div className="line-clamp-2 text-[13px] font-semibold leading-5 text-white">
|
||||
{preset.name}
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-col gap-1 text-[10px] text-white/50">
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{compactSummaryBadges.map((badge, badgeIndex) => (
|
||||
<span
|
||||
key={`${preset.id}-badge-${badgeIndex}-${badge}`}
|
||||
className={cn(
|
||||
"rounded-full border px-2 py-1 text-[9px] leading-none",
|
||||
badgeIndex === 0
|
||||
? "border-emerald-400/25 bg-emerald-500/[0.12] text-emerald-100"
|
||||
: "border-white/10 bg-white/[0.04] text-white/62"
|
||||
)}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] leading-4 text-white/45">
|
||||
<div className="truncate">
|
||||
默认模型:{preset.defaultModel?.displayName || "未配置"}
|
||||
</div>
|
||||
{preset.description ? (
|
||||
<div className="line-clamp-1">{preset.description}</div>
|
||||
) : categoryHint ? (
|
||||
<div className="truncate">适用分类:{categoryHint}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-1 text-[10px] text-white/50">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<User className="h-3 w-3 text-white/34" />
|
||||
<span className="truncate">贡献者:{ownerName}</span>
|
||||
@ -339,6 +398,22 @@ export function AgentPresetCardDark({
|
||||
<h3 className="mb-2 line-clamp-1 text-base font-semibold text-white drop-shadow-lg">
|
||||
{preset.name}
|
||||
</h3>
|
||||
|
||||
<div className="mb-2 flex flex-wrap gap-1.5">
|
||||
{compactSummaryBadges.slice(0, 4).map((badge, badgeIndex) => (
|
||||
<span
|
||||
key={`${preset.id}-full-badge-${badgeIndex}-${badge}`}
|
||||
className={cn(
|
||||
"rounded-full border px-2.5 py-1 text-[10px] font-medium backdrop-blur-md",
|
||||
badgeIndex === 0
|
||||
? "border-emerald-300/35 bg-emerald-500/[0.16] text-emerald-100"
|
||||
: "border-white/10 bg-black/35 text-white/72"
|
||||
)}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 元信息 */}
|
||||
<div className="flex items-center justify-between text-xs text-white/60">
|
||||
@ -347,12 +422,12 @@ export function AgentPresetCardDark({
|
||||
<User className="h-3 w-3" />
|
||||
{ownerName}
|
||||
</span>
|
||||
{preset.slotCount > 0 && (
|
||||
{preset.defaultModel?.displayName ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
{preset.slotCount}槽
|
||||
{preset.defaultModel.displayName}
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
{publishedAt && (
|
||||
<span className="flex items-center gap-1">
|
||||
|
||||
@ -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({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-3 rounded-[24px] border px-4 py-4",
|
||||
isDark ? "border-white/10 bg-black/25" : "border-[#dbe3ff] bg-white"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "text-[#334155]")}>
|
||||
选图流程
|
||||
</div>
|
||||
<div className={cn("text-xs leading-5", isDark ? "text-white/50" : "text-[#64748b]")}>
|
||||
按这 3 步操作就能完成输入图准备,避免来回试错。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-3">
|
||||
{materialGuideSteps.map((step) => (
|
||||
<div
|
||||
key={step.badge}
|
||||
className={cn(
|
||||
"rounded-[20px] border px-4 py-3",
|
||||
step.status === "completed"
|
||||
? isDark
|
||||
? "border-emerald-400/20 bg-emerald-500/[0.08]"
|
||||
: "border-emerald-200 bg-emerald-50"
|
||||
: step.status === "current"
|
||||
? isDark
|
||||
? "border-amber-400/20 bg-amber-500/[0.08]"
|
||||
: "border-amber-200 bg-amber-50"
|
||||
: isDark
|
||||
? "border-white/10 bg-white/[0.03]"
|
||||
: "border-[#dbe3ff] bg-[#f8faff]"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2.5 py-1 text-[11px] font-semibold",
|
||||
step.status === "completed"
|
||||
? isDark
|
||||
? "bg-emerald-300 text-black"
|
||||
: "bg-emerald-600 text-white"
|
||||
: step.status === "current"
|
||||
? isDark
|
||||
? "bg-amber-300 text-black"
|
||||
: "bg-amber-500 text-white"
|
||||
: isDark
|
||||
? "bg-white/10 text-white/60"
|
||||
: "bg-white text-[#64748b]"
|
||||
)}
|
||||
>
|
||||
{step.badge}
|
||||
</span>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "text-[#334155]")}>
|
||||
{step.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("mt-2 text-xs leading-5", isDark ? "text-white/50" : "text-[#64748b]")}>
|
||||
{step.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{slotPreviewItems.length > 0 ? (
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-3 rounded-[24px] border px-4 py-4",
|
||||
isDark ? "border-white/10 bg-black/25" : "border-[#dbe3ff] bg-white"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "text-[#334155]")}>
|
||||
产品图槽位映射
|
||||
</div>
|
||||
<div className={cn("text-xs leading-5", isDark ? "text-white/50" : "text-[#64748b]")}>
|
||||
当前已选图片会按顺序自动带入后续生图槽位,先主商品,后搭配商品。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 xl:grid-cols-4">
|
||||
{slotPreviewItems.map((item, index) => {
|
||||
const source = item.assigned?.source || null;
|
||||
const previewUrl = item.assigned?.image.imageUrl || "";
|
||||
const isAssigned = Boolean(item.assigned && previewUrl);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${item.slotLabel}-${index}`}
|
||||
className={cn(
|
||||
"overflow-hidden rounded-[22px] border",
|
||||
isAssigned
|
||||
? isDark
|
||||
? "border-emerald-400/20 bg-white/[0.03]"
|
||||
: "border-[#c7d2fe] bg-[#f8faff]"
|
||||
: isDark
|
||||
? "border-dashed border-white/10 bg-white/[0.02]"
|
||||
: "border-dashed border-[#dbe3ff] bg-[#f8faff]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative aspect-[5/4] overflow-hidden",
|
||||
isAssigned ? "" : isDark ? "bg-black/30" : "bg-white"
|
||||
)}
|
||||
>
|
||||
{isAssigned ? (
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={item.assigned?.image.label || item.slotLabel}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center text-[11px]",
|
||||
isDark ? "text-white/30" : "text-[#94a3b8]"
|
||||
)}
|
||||
>
|
||||
待选择
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute left-2 top-2 rounded-full bg-black/60 px-2 py-1 text-[10px] text-white">
|
||||
{item.slotLabel}
|
||||
</div>
|
||||
<div className="absolute right-2 top-2 rounded-full border border-white/10 bg-black/45 px-2 py-1 text-[10px] text-white/78">
|
||||
{source || "空槽位"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 border-t border-white/10 px-3 py-2">
|
||||
<div className={cn("truncate text-xs font-medium", isDark ? "text-white/82" : "text-[#334155]")}>
|
||||
{isAssigned
|
||||
? item.assigned?.image.label || `已映射到${item.slotLabel}`
|
||||
: `${item.slotLabel}待补图`}
|
||||
</div>
|
||||
<div className={cn("truncate text-[11px]", isDark ? "text-white/45" : "text-[#64748b]")}>
|
||||
{isAssigned
|
||||
? `${source}第 ${(item.assigned?.sourceIndex || 0) + 1} 张`
|
||||
: "先在下方搜索并选择图片"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<AgentWorkflowMaterialPicker
|
||||
value={inputs.primarySku}
|
||||
queryMode={materialSearchMode.primary}
|
||||
|
||||
@ -93,6 +93,22 @@ function buildSelectionSummary(params: {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getWorkflowStepSelectionSummary(params: {
|
||||
step: AgentWorkflowUiConfig["steps"][number];
|
||||
selectionValue?: string;
|
||||
}) {
|
||||
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||||
step: params.step,
|
||||
selectionValue: params.selectionValue,
|
||||
});
|
||||
|
||||
if (selectedOptions.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return selectedOptions.map((option) => 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<string, string>;
|
||||
}, [resolvedConfig.steps, selections]);
|
||||
const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState<string>("");
|
||||
const [pendingAutoAdvanceStepKey, setPendingAutoAdvanceStepKey] = useState<string | null>(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 (
|
||||
<button
|
||||
@ -507,7 +534,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
setActiveEmbeddedStepKey(step.key);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-colors",
|
||||
"flex items-start gap-2 rounded-[18px] border px-3 py-2 text-left transition-colors",
|
||||
active
|
||||
? "border-emerald-400/45 bg-emerald-500/[0.12] text-emerald-100"
|
||||
: completed
|
||||
@ -527,7 +554,21 @@ export function AgentWorkflowPanelDarkV2({
|
||||
>
|
||||
{stepIndex + 1}
|
||||
</span>
|
||||
<span>{step.title}</span>
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<span>{step.title}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 max-w-[180px] truncate text-[10px]",
|
||||
active
|
||||
? "text-emerald-100/80"
|
||||
: completed
|
||||
? "text-white/52"
|
||||
: "text-white/32"
|
||||
)}
|
||||
>
|
||||
{stepSummary || "待选择"}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@ -545,6 +586,11 @@ export function AgentWorkflowPanelDarkV2({
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-2 text-xs text-white/55">
|
||||
{stepSelectionSummaryMap[activeEmbeddedStepEntry.step.key]
|
||||
? `已选:${stepSelectionSummaryMap[activeEmbeddedStepEntry.step.key]}`
|
||||
: "待选择:完成当前卡片后会自动推进到下一步"}
|
||||
</div>
|
||||
{activeEmbeddedStepEntry.step.description ? (
|
||||
<p className="text-sm text-white/40">
|
||||
{activeEmbeddedStepEntry.step.description}
|
||||
@ -582,6 +628,11 @@ export function AgentWorkflowPanelDarkV2({
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
<div className="mb-3 text-xs text-white/55">
|
||||
{stepSelectionSummaryMap[step.key]
|
||||
? `已选:${stepSelectionSummaryMap[step.key]}`
|
||||
: "待选择:先确定这一组风格卡片,系统再继续组合 Prompt"}
|
||||
</div>
|
||||
{step.description && (
|
||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user