feat: improve agent workflow guidance
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 22:10:14 +08:00
parent 656aacbd01
commit c0d4375daa
4 changed files with 554 additions and 8 deletions

View File

@ -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">

View File

@ -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">

View File

@ -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}

View File

@ -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>
)}