feat: unify agent workflow into anchored single page
This commit is contained in:
parent
44b2148bef
commit
9631c10812
@ -1464,41 +1464,6 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
) : null;
|
||||
|
||||
if (isAgentMode) {
|
||||
// 深色主题智能体模式 V2 - 完整功能版本
|
||||
|
||||
// 第一步:工作流配置(品牌/店铺/步骤选择)
|
||||
if (currentAgentStage === "workflow") {
|
||||
return (
|
||||
<AgentWorkflowPanelDarkV2
|
||||
config={resolvedWorkflowUiConfig}
|
||||
inputs={workflowInputs}
|
||||
onInputsChange={setWorkflowInputs}
|
||||
materials={workflowMaterials}
|
||||
onMaterialsChange={setWorkflowMaterials}
|
||||
selections={workflowSelections}
|
||||
onSelectionsChange={setWorkflowSelections}
|
||||
siteName={siteSettingsData?.siteName}
|
||||
siteLogo={siteSettingsData?.siteLogo}
|
||||
siteLogoDark={siteSettingsData?.siteLogoDark}
|
||||
showcaseConfig={siteSettingsData?.agentWorkflowShowcaseConfig}
|
||||
topNavRightSlot={agentProjectNavSlot}
|
||||
onNextStep={() => setCurrentAgentStage("mode")}
|
||||
onSkipStep={() => setCurrentAgentStage("mode")}
|
||||
onReset={() => {
|
||||
setWorkflowInputs({ primarySku: "", secondarySku: "", brand: "", store: "" });
|
||||
setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections());
|
||||
setWorkflowSelections({});
|
||||
setSelectedBrandModel(null);
|
||||
setSelectedAgentPresetId(null);
|
||||
setAgentModeState(createEmptyAgentModeWorkspaceState());
|
||||
setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState());
|
||||
setCurrentAgentStage("workflow");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 第二步:智能体选择 + 生图工作台
|
||||
return (
|
||||
<AgentGenerationWorkspaceDarkV2
|
||||
chatId={agentModeChatId}
|
||||
@ -1514,16 +1479,27 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
siteName={siteSettingsData?.siteName}
|
||||
siteLogo={siteSettingsData?.siteLogo}
|
||||
siteLogoDark={siteSettingsData?.siteLogoDark}
|
||||
showcaseConfig={siteSettingsData?.agentWorkflowShowcaseConfig}
|
||||
topNavRightSlot={agentProjectNavSlot}
|
||||
syncGeneratedResultsToCanvas={false}
|
||||
onBack={() => setCurrentAgentStage("workflow")}
|
||||
onOpenCanvasEditor={() => {
|
||||
setCurrentAgentStage("mode");
|
||||
updateCanvasSearchParams({
|
||||
mode: null,
|
||||
});
|
||||
}}
|
||||
onClearPreset={() => setSelectedAgentPresetId(null)}
|
||||
workflowStylePanel={
|
||||
<AgentWorkflowPanelDarkV2
|
||||
embedded
|
||||
config={resolvedWorkflowUiConfig}
|
||||
inputs={workflowInputs}
|
||||
onInputsChange={setWorkflowInputs}
|
||||
materials={workflowMaterials}
|
||||
onMaterialsChange={setWorkflowMaterials}
|
||||
selections={workflowSelections}
|
||||
onSelectionsChange={setWorkflowSelections}
|
||||
/>
|
||||
}
|
||||
// 嵌入的智能体选择面板
|
||||
presetPanel={
|
||||
<AgentPresetPanelDarkV2
|
||||
|
||||
@ -30,6 +30,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||
import { ImageGenerationLoading } from "@/components/image-generation-loading";
|
||||
import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner";
|
||||
import { useCanvasStore } from "@/store/canvas";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||
@ -46,6 +47,7 @@ import type {
|
||||
AgentModeReferenceCandidate,
|
||||
AgentModeWorkspaceState,
|
||||
AgentPreset,
|
||||
AgentWorkflowShowcaseConfig,
|
||||
AgentWorkflowComposerContext,
|
||||
AgentWorkflowMaterialQueryType,
|
||||
AgentWorkflowSelectedBrandModel,
|
||||
@ -80,6 +82,8 @@ interface AgentGenerationWorkspaceDarkV2Props {
|
||||
onWorkspaceStateChange?: (next: AgentGenerationProjectWorkspaceState) => void;
|
||||
onResultPreviewUrlsChange?: (urls: string[]) => void;
|
||||
topNavRightSlot?: ReactNode;
|
||||
showcaseConfig?: AgentWorkflowShowcaseConfig | null;
|
||||
workflowStylePanel?: ReactNode;
|
||||
}
|
||||
|
||||
interface PickedAttachment extends Attachment {}
|
||||
@ -297,6 +301,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
onWorkspaceStateChange,
|
||||
onResultPreviewUrlsChange,
|
||||
topNavRightSlot,
|
||||
showcaseConfig,
|
||||
workflowStylePanel,
|
||||
}: AgentGenerationWorkspaceDarkV2Props) {
|
||||
const darkSelectContentClass =
|
||||
"border-white/10 bg-zinc-950 text-white shadow-2xl [--popover:hsl(240_10%_3.9%)] [--popover-foreground:hsl(0_0%_98%)] [--accent:rgba(255,255,255,0.08)] [--accent-foreground:hsl(0_0%_98%)] [--muted:rgba(255,255,255,0.04)] [--muted-foreground:rgba(255,255,255,0.56)]";
|
||||
@ -521,6 +527,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const [activeToolStep, setActiveToolStep] = useState<ToolStepKey>("preset");
|
||||
const hasBrandModelStep = Boolean(brandModelPanel) && modelSlotEnabled;
|
||||
const hasMaterialStep = Boolean(materialSelectionPanel);
|
||||
const hasWorkflowStyleStep = Boolean(workflowStylePanel) || hasBrandModelStep;
|
||||
|
||||
const { messages, setMessages, sendMessage, status } = useChat<ChatMessage>({
|
||||
id: chatId,
|
||||
@ -763,6 +770,20 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const latestReplacementResultUrls = latestGeneratedGroup?.imageUrls || [];
|
||||
const displayedResultUrl = selectedResultUrl || latestGeneratedGroup?.imageUrls[0] || null;
|
||||
const isGenerating = status === "streaming" || status === "submitted";
|
||||
const sceneResultCardCount = Math.max(
|
||||
allGeneratedImages.length,
|
||||
imageParams.imageCount || 1,
|
||||
1
|
||||
);
|
||||
const sceneResultCards = useMemo(
|
||||
() =>
|
||||
Array.from({ length: sceneResultCardCount }, (_, index) => ({
|
||||
id: allGeneratedImages[index] || `scene-result-placeholder-${index}`,
|
||||
index,
|
||||
url: allGeneratedImages[index] || null,
|
||||
})),
|
||||
[allGeneratedImages, sceneResultCardCount]
|
||||
);
|
||||
const attachmentCount = attachments.filter(Boolean).length;
|
||||
const hasBrandModelSelection = Boolean(
|
||||
selectedBrandModel?.imageUrl || referenceImages.some((image) => image.source === "brandModel")
|
||||
@ -775,6 +796,58 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const mergedSettingsStepDescription = hasMaterialStep
|
||||
? "搜索素材、补充要求、确认槽位并开始生成"
|
||||
: "补充要求并开始生成";
|
||||
const styleStepBadge = hasMaterialStep ? "03" : "02";
|
||||
const generateStepBadge =
|
||||
hasMaterialStep && hasWorkflowStyleStep
|
||||
? "04"
|
||||
: hasMaterialStep || hasWorkflowStyleStep
|
||||
? "03"
|
||||
: "02";
|
||||
const unifiedAnchorSteps = useMemo(
|
||||
() => {
|
||||
const steps = [
|
||||
{
|
||||
id: "agent-anchor-mode",
|
||||
badge: "01",
|
||||
label: "选择生图模式",
|
||||
description: "先确定本次要执行的生图模式",
|
||||
},
|
||||
];
|
||||
|
||||
if (hasMaterialStep) {
|
||||
steps.push({
|
||||
id: "agent-anchor-materials",
|
||||
badge: "02",
|
||||
label: "选择 SPU / SKU",
|
||||
description: "查询商品图并确定本次输入素材",
|
||||
});
|
||||
}
|
||||
|
||||
if (hasWorkflowStyleStep) {
|
||||
steps.push({
|
||||
id: "agent-anchor-style",
|
||||
badge: hasMaterialStep ? "03" : "02",
|
||||
label: "风格步骤卡片",
|
||||
description: "补齐风格、品牌和模特等工作流配置",
|
||||
});
|
||||
}
|
||||
|
||||
steps.push({
|
||||
id: "agent-anchor-generate",
|
||||
badge:
|
||||
hasMaterialStep && hasWorkflowStyleStep
|
||||
? "04"
|
||||
: hasMaterialStep || hasWorkflowStyleStep
|
||||
? "03"
|
||||
: "02",
|
||||
label: "生图与结果预览",
|
||||
description: "确认参数后直接生成,并在底部查看结果",
|
||||
});
|
||||
|
||||
return steps;
|
||||
},
|
||||
[hasMaterialStep, hasWorkflowStyleStep]
|
||||
);
|
||||
|
||||
const toolSteps = useMemo(
|
||||
() => [
|
||||
@ -1250,6 +1323,16 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
},
|
||||
[toolSteps]
|
||||
);
|
||||
const scrollToSection = useCallback((sectionId: string) => {
|
||||
if (typeof document === "undefined") {
|
||||
return;
|
||||
}
|
||||
const target = document.getElementById(sectionId);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}, []);
|
||||
const updateDetailReplacementState = useCallback(
|
||||
(
|
||||
updater:
|
||||
@ -1645,66 +1728,113 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto overscroll-contain pt-16">
|
||||
<div className="mx-auto flex w-full max-w-[1480px] flex-col gap-6 px-6 py-6 pb-24 lg:px-8 xl:px-10">
|
||||
{showcaseConfig ? (
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.04 }}
|
||||
>
|
||||
<AgentWorkflowShowcaseBanner
|
||||
config={showcaseConfig}
|
||||
actionLabel="开始选择模式"
|
||||
secondaryActionLabel={allGeneratedImages.length > 0 ? "查看结果预览" : undefined}
|
||||
onActionClick={() => scrollToSection("agent-anchor-mode")}
|
||||
onSecondaryActionClick={() => scrollToSection("agent-anchor-results")}
|
||||
/>
|
||||
</motion.section>
|
||||
) : null}
|
||||
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.06 }}
|
||||
className="rounded-[28px] border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
||||
>
|
||||
<div className="flex flex-col gap-4 border-b border-white/10 pb-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-white/35">Current Module</div>
|
||||
<h3 className="text-lg font-semibold text-white">{activeToolStepMeta.label}</h3>
|
||||
<p className="text-sm text-white/45">{activeToolStepMeta.description}</p>
|
||||
{isReplacementPreset ? (
|
||||
<p className="text-xs text-amber-300/80">
|
||||
{isDetailReplacementPreset
|
||||
? "当前模板属于详情替换模式,下一步将切入专用工作台进行逐屏替换。"
|
||||
: "当前模板属于 SKU 替换模式,下一步将切入专用工作台进行一对一替换。"}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="mb-5 border-b border-white/10 pb-5">
|
||||
<div className="mb-4 space-y-1">
|
||||
<div className="text-xs uppercase tracking-[0.2em] text-white/35">Workflow Anchors</div>
|
||||
<h3 className="text-lg font-semibold text-white">单页生图流程</h3>
|
||||
<p className="text-sm text-white/45">
|
||||
在同一页面完成生图模式选择、SPU / SKU 选图、风格步骤配置、生成和结果预览。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{selectedAgentPreset ? (
|
||||
<div className="flex items-center gap-3 rounded-[20px] border border-white/10 bg-black/20 px-3 py-2">
|
||||
{selectedAgentPreset.avatarUrl ? (
|
||||
<img
|
||||
src={selectedAgentPreset.avatarUrl}
|
||||
alt={selectedAgentPreset.name}
|
||||
className="h-10 w-10 rounded-xl object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/18">
|
||||
<Wand2 className="h-5 w-5 text-emerald-300" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">{selectedAgentPreset.name}</div>
|
||||
<div className="text-xs text-white/40">
|
||||
{selectedAgentPreset.type === "image" ? "生图模式" : "视频模式"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedAgentPreset && onClearPreset ? (
|
||||
<div className="agent-dark-scrollbar flex gap-2 overflow-x-auto pb-1">
|
||||
{unifiedAnchorSteps.map((step) => (
|
||||
<button
|
||||
key={step.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClearPreset();
|
||||
setActiveToolStep("preset");
|
||||
}}
|
||||
className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-emerald-300 transition-colors hover:border-emerald-300/35 hover:text-emerald-200"
|
||||
onClick={() => scrollToSection(step.id)}
|
||||
className="flex min-w-max items-center gap-3 rounded-full border border-white/10 bg-white/[0.03] px-4 py-2 text-left transition-colors hover:border-white/20 hover:bg-white/[0.06]"
|
||||
>
|
||||
更换生图模式
|
||||
<span className="rounded-full bg-white text-[10px] font-semibold text-black px-2 py-0.5">
|
||||
{step.badge}
|
||||
</span>
|
||||
<span className="flex flex-col">
|
||||
<span className="text-sm font-medium text-white">{step.label}</span>
|
||||
<span className="text-[11px] text-white/45">{step.description}</span>
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 min-h-[260px]">
|
||||
<div className={cn(activeToolStep === "preset" ? "block" : "hidden")}>
|
||||
<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">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
|
||||
01
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-white">选择生图模式</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/45">
|
||||
先确定本次使用的模式,后续的商品图、参考图和结果展示都会随模式切换。
|
||||
</p>
|
||||
{isReplacementPreset ? (
|
||||
<p className="text-xs text-amber-300/80">
|
||||
{isDetailReplacementPreset
|
||||
? "当前模板属于详情替换模式,结果区会按参考图顺序逐屏展示。"
|
||||
: "当前模板属于 SKU 替换模式,结果区会按参考图顺序一对一展示。"}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{selectedAgentPreset ? (
|
||||
<div className="flex items-center gap-3 rounded-[20px] border border-white/10 bg-black/20 px-3 py-2">
|
||||
{selectedAgentPreset.avatarUrl ? (
|
||||
<img
|
||||
src={selectedAgentPreset.avatarUrl}
|
||||
alt={selectedAgentPreset.name}
|
||||
className="h-10 w-10 rounded-xl object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/18">
|
||||
<Wand2 className="h-5 w-5 text-emerald-300" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">{selectedAgentPreset.name}</div>
|
||||
<div className="text-xs text-white/40">
|
||||
{selectedAgentPreset.type === "image" ? "生图模式" : "视频模式"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedAgentPreset && onClearPreset ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearPreset}
|
||||
className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-emerald-300 transition-colors hover:border-emerald-300/35 hover:text-emerald-200"
|
||||
>
|
||||
更换生图模式
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{presetPanel ? (
|
||||
<div className="agent-dark-scrollbar max-h-[520px] overflow-y-auto pr-1">
|
||||
<div className="agent-dark-scrollbar max-h-[760px] overflow-y-auto pr-1">
|
||||
{presetPanel}
|
||||
</div>
|
||||
) : (
|
||||
@ -1712,17 +1842,60 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
暂无可用的生图模式配置。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{hasBrandModelStep ? (
|
||||
<div className={cn(activeToolStep === "brand-model" ? "block" : "hidden")}>
|
||||
{brandModelPanel}
|
||||
</div>
|
||||
{hasMaterialStep ? (
|
||||
<section id="agent-anchor-materials" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="mb-4 space-y-1 border-b border-white/10 pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
|
||||
02
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-white">选择 SPU / SKU</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/45">
|
||||
先搜索商品白底图与搭配图,再继续完成风格步骤和生成参数配置。
|
||||
</p>
|
||||
</div>
|
||||
{materialSelectionPanel}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<div className={cn(activeToolStep === "settings" ? "block" : "hidden")}>
|
||||
{hasWorkflowStyleStep ? (
|
||||
<section id="agent-anchor-style" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="mb-4 space-y-1 border-b border-white/10 pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
|
||||
{styleStepBadge}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-white">风格步骤卡片</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/45">
|
||||
在同一页完成品牌、店铺、风格卡片、品牌模特和 Prompt 组合,不再跳转第二个页面。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{hasBrandModelStep ? brandModelPanel : null}
|
||||
{workflowStylePanel}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section id="agent-anchor-generate" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="mb-4 space-y-1 border-b border-white/10 pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
|
||||
{generateStepBadge}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-white">生图与结果预览</span>
|
||||
</div>
|
||||
<p className="text-sm text-white/45">
|
||||
确认补充要求、输入图槽位和生成参数后直接开始生成,结果会显示在当前页底部预览区。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{hasMaterialStep ? materialSelectionPanel : null}
|
||||
<div className="rounded-[28px] border border-white/12 bg-white/[0.02] p-4">
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<Sparkles className="h-4 w-4 text-emerald-300" />
|
||||
@ -1735,126 +1908,93 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
className="min-h-[160px] resize-none rounded-[24px] border-white/12 bg-black text-sm text-white placeholder:text-white/20 focus:border-emerald-400/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{attachmentSlotPanel}
|
||||
|
||||
<div className="rounded-[28px] border border-white/12 bg-white/[0.02] p-4">
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<Settings2 className="h-4 w-4 text-emerald-300" />
|
||||
生成参数
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{isReplacementPreset ? (
|
||||
<div className="rounded-[24px] border border-amber-400/20 bg-amber-500/[0.08] px-4 py-3 text-sm text-amber-100/90">
|
||||
{isDetailReplacementPreset
|
||||
? "详情替换会按当前参考图顺序逐张执行,结果将在下方结果区一一回显。"
|
||||
: "SKU替换会按当前参考图顺序执行一对一替换,结果将在下方结果区一一回显。"}
|
||||
</div>
|
||||
) : null}
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<Settings2 className="h-4 w-4 text-emerald-300" />
|
||||
生成参数
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{isReplacementPreset ? (
|
||||
<div className="rounded-[24px] border border-amber-400/20 bg-amber-500/[0.08] px-4 py-3 text-sm text-amber-100/90">
|
||||
{isDetailReplacementPreset
|
||||
? "详情替换会按当前参考图顺序逐张执行,结果将在下方结果区一一回显。"
|
||||
: "SKU替换会按当前参考图顺序执行一对一替换,结果将在下方结果区一一回显。"}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{shouldAllowModelOverride ? (
|
||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||
<SelectTrigger className="h-10 rounded-[24px] border-white/12 bg-black text-white">
|
||||
<SelectValue placeholder="选择模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className={darkSelectContentClass}>
|
||||
{selectableModels.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
className="text-white focus:bg-white/10 focus:text-white"
|
||||
>
|
||||
{model.displayName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="rounded-[24px] border border-white/10 bg-black/30 px-4 py-3 text-sm text-white/70">
|
||||
当前模板已锁定模型:
|
||||
<span className="ml-2 font-medium text-white">
|
||||
{selectedAgentPreset?.defaultModel?.displayName || currentModel?.displayName || "未配置"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ImageParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
onParamsChange={setImageParams}
|
||||
inline
|
||||
inlineTheme="dark"
|
||||
showInlineDivider={false}
|
||||
providerType={currentModel?.providerType}
|
||||
imageGenConfig={currentModel?.imageGenConfig || undefined}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={isGenerating || !selectedAgentPresetId}
|
||||
className={cn(
|
||||
"h-11 w-full gap-2 rounded-[24px]",
|
||||
"bg-gradient-to-r from-emerald-500 to-teal-500",
|
||||
"text-white font-medium",
|
||||
"hover:from-emerald-400 hover:to-teal-400",
|
||||
"shadow-[0_0_24px_rgba(16,185,129,0.24)]",
|
||||
"disabled:opacity-30"
|
||||
)}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{isReplacementPreset ? "批量替换中..." : "生成中..."}
|
||||
</>
|
||||
{shouldAllowModelOverride ? (
|
||||
<Select value={selectedModelId} onValueChange={setSelectedModelId}>
|
||||
<SelectTrigger className="h-10 rounded-[24px] border-white/12 bg-black text-white">
|
||||
<SelectValue placeholder="选择模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className={darkSelectContentClass}>
|
||||
{selectableModels.map((model) => (
|
||||
<SelectItem
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
className="text-white focus:bg-white/10 focus:text-white"
|
||||
>
|
||||
{model.displayName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
{isReplacementPreset ? "开始批量替换" : "开始生成"}
|
||||
</>
|
||||
<div className="rounded-[24px] border border-white/10 bg-black/30 px-4 py-3 text-sm text-white/70">
|
||||
当前模板已锁定模型:
|
||||
<span className="ml-2 font-medium text-white">
|
||||
{selectedAgentPreset?.defaultModel?.displayName || currentModel?.displayName || "未配置"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<ImageParamsBar
|
||||
modelName={currentModel?.displayName}
|
||||
initialParams={imageParams}
|
||||
onParamsChange={setImageParams}
|
||||
inline
|
||||
inlineTheme="dark"
|
||||
showInlineDivider={false}
|
||||
providerType={currentModel?.providerType}
|
||||
imageGenConfig={currentModel?.imageGenConfig || undefined}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={isGenerating || !selectedAgentPresetId}
|
||||
className={cn(
|
||||
"h-11 w-full gap-2 rounded-[24px]",
|
||||
"bg-gradient-to-r from-emerald-500 to-teal-500",
|
||||
"text-white font-medium",
|
||||
"hover:from-emerald-400 hover:to-teal-400",
|
||||
"shadow-[0_0_24px_rgba(16,185,129,0.24)]",
|
||||
"disabled:opacity-30"
|
||||
)}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{isReplacementPreset ? "批量替换中..." : "生成中..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
{isReplacementPreset ? "开始批量替换" : "开始生成"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-white/10 pt-4">
|
||||
<div className="text-xs text-white/40">
|
||||
{hasMaterialStep
|
||||
? "当前页已合并 SKU / SPU 选图、补充要求、槽位确认和生成参数,可在这里直接完成后开始生成。"
|
||||
: "当前页已集中补充要求与生成参数,确认后即可直接开始生成。"}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => previousToolStep && handleToolStepChange(previousToolStep.key)}
|
||||
disabled={!previousToolStep}
|
||||
className={cn(
|
||||
"rounded-full border px-4 py-2 text-sm transition-colors",
|
||||
previousToolStep
|
||||
? "border-white/12 bg-white/[0.03] text-white hover:border-white/20 hover:bg-white/[0.06]"
|
||||
: "cursor-not-allowed border-white/8 bg-white/[0.02] text-white/25"
|
||||
)}
|
||||
>
|
||||
上一步
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => nextToolStep && handleToolStepChange(nextToolStep.key)}
|
||||
disabled={!nextToolStep}
|
||||
className={cn(
|
||||
"rounded-full border px-4 py-2 text-sm transition-colors",
|
||||
nextToolStep
|
||||
? "border-emerald-400/35 bg-emerald-500/[0.08] text-emerald-200 hover:border-emerald-300/50 hover:bg-emerald-500/[0.12]"
|
||||
: "cursor-not-allowed border-white/8 bg-white/[0.02] text-white/25"
|
||||
)}
|
||||
>
|
||||
下一步
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<motion.section
|
||||
id="agent-anchor-results"
|
||||
className="relative flex min-h-[620px] flex-col overflow-hidden rounded-[28px] border border-white/12 bg-white/[0.04] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
@ -2616,29 +2756,118 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
phase="processing"
|
||||
/>
|
||||
</motion.div>
|
||||
) : displayedResultUrl ? (
|
||||
) : allGeneratedImages.length > 0 ? (
|
||||
<motion.div
|
||||
key="result"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="relative flex flex-1 items-center justify-center p-6"
|
||||
className="flex flex-1 flex-col p-5"
|
||||
>
|
||||
<img
|
||||
src={buildPreviewImageUrl(displayedResultUrl, 1400)}
|
||||
alt="生成结果"
|
||||
className="max-h-full max-w-full cursor-zoom-in rounded-[24px] object-contain"
|
||||
onClick={() => setFullscreenPreview(displayedResultUrl)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
void handleDownloadOriginalImage(displayedResultUrl);
|
||||
}}
|
||||
className="absolute bottom-6 right-6 flex h-10 items-center gap-2 rounded-full bg-white/10 px-4 text-sm text-white backdrop-blur-md transition-colors hover:bg-white/20"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
下载
|
||||
</button>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="text-sm text-white/45">
|
||||
单击卡片高亮,双击放大预览
|
||||
</div>
|
||||
{selectedResultUrl ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleDownloadOriginalImage(selectedResultUrl);
|
||||
}}
|
||||
className="flex h-9 items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 text-sm text-white transition-colors hover:border-white/20 hover:bg-white/[0.08]"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
下载当前
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
{sceneResultCards.map((card) => {
|
||||
const isActive = card.url && displayedResultUrl === card.url;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={card.id}
|
||||
role={card.url ? "button" : undefined}
|
||||
tabIndex={card.url ? 0 : -1}
|
||||
onClick={() => {
|
||||
if (!card.url) {
|
||||
return;
|
||||
}
|
||||
setSelectedResultUrl(card.url);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (!card.url) {
|
||||
return;
|
||||
}
|
||||
setSelectedResultUrl(card.url);
|
||||
setFullscreenPreview(card.url);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!card.url) {
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
setSelectedResultUrl(card.url);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"group overflow-hidden rounded-[22px] border text-left transition-all",
|
||||
card.url ? "cursor-pointer" : "cursor-default",
|
||||
isActive
|
||||
? "border-emerald-400/55 bg-emerald-500/[0.08] ring-1 ring-emerald-400/35"
|
||||
: "border-white/10 bg-black/20 hover:border-white/18"
|
||||
)}
|
||||
>
|
||||
<div className="relative aspect-[4/5] overflow-hidden bg-[linear-gradient(180deg,#090b0c_0%,#111417_100%)]">
|
||||
{card.url ? (
|
||||
<>
|
||||
<img
|
||||
src={buildPreviewImageUrl(card.url, 480)}
|
||||
alt={`结果图 ${card.index + 1}`}
|
||||
className="h-full w-full object-contain p-2"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void handleDownloadOriginalImage(card.url!);
|
||||
}}
|
||||
className="absolute right-3 top-3 rounded-full border border-white/10 bg-black/55 px-2.5 py-1 text-[11px] text-white/78 transition-colors hover:border-white/20 hover:text-white"
|
||||
>
|
||||
下载
|
||||
</button>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/35 to-transparent px-3 py-3">
|
||||
<div className="text-sm font-medium text-white/88">
|
||||
结果图 {card.index + 1}
|
||||
</div>
|
||||
<div className="text-[11px] text-white/45">
|
||||
双击可放大查看
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-white/5">
|
||||
<Wand2 className="h-6 w-6 text-white/20" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-white/60">
|
||||
结果图 {card.index + 1}
|
||||
</div>
|
||||
<div className="mt-2 text-xs leading-5 text-white/32">
|
||||
{isGenerating
|
||||
? "正在生成中,结果会按卡片顺序依次填充。"
|
||||
: "开始生成后,这里会显示固定尺寸预览卡片。"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
@ -2664,35 +2893,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{allGeneratedImages.length > 0 ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="border-t border-white/12 bg-black/20 px-4 py-3"
|
||||
>
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
{allGeneratedImages.map((url) => (
|
||||
<button
|
||||
key={url}
|
||||
onClick={() => setSelectedResultUrl(url)}
|
||||
className={cn(
|
||||
"relative flex-shrink-0 overflow-hidden rounded-[24px] transition-all",
|
||||
displayedResultUrl === url
|
||||
? "ring-2 ring-emerald-400"
|
||||
: "ring-1 ring-white/12 hover:ring-white/30"
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={buildPreviewImageUrl(url, 240)}
|
||||
alt="缩略图"
|
||||
className="h-20 w-20 object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</motion.section>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@ -17,6 +17,7 @@ import {
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
getEnabledAgentWorkflowOptionTree,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
isAgentWorkflowStepSelectionComplete,
|
||||
serializeAgentWorkflowSelectionPath,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||
@ -56,6 +57,7 @@ interface AgentWorkflowPanelDarkV2Props {
|
||||
siteLogoDark?: string | null;
|
||||
showcaseConfig?: AgentWorkflowShowcaseConfig | null;
|
||||
topNavRightSlot?: ReactNode;
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
function buildSelectionSummary(params: {
|
||||
@ -183,6 +185,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
siteLogoDark,
|
||||
showcaseConfig,
|
||||
topNavRightSlot,
|
||||
embedded = false,
|
||||
}: AgentWorkflowPanelDarkV2Props) {
|
||||
const brandOptions = useMemo(
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
@ -231,6 +234,70 @@ export function AgentWorkflowPanelDarkV2({
|
||||
() => getAgentWorkflowProgress({ config: resolvedConfig, selections }),
|
||||
[resolvedConfig, selections]
|
||||
);
|
||||
const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState<string>("");
|
||||
const activeEmbeddedStepIndex = Math.max(
|
||||
flattenedResolvedSteps.findIndex(({ step }) => step.key === activeEmbeddedStepKey),
|
||||
0
|
||||
);
|
||||
const activeEmbeddedStepEntry =
|
||||
flattenedResolvedSteps[activeEmbeddedStepIndex] || flattenedResolvedSteps[0] || null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!embedded) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flattenedResolvedSteps.length === 0) {
|
||||
if (activeEmbeddedStepKey) {
|
||||
setActiveEmbeddedStepKey("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
activeEmbeddedStepKey &&
|
||||
flattenedResolvedSteps.some(({ step }) => step.key === activeEmbeddedStepKey)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveEmbeddedStepKey(flattenedResolvedSteps[0]?.step.key || "");
|
||||
}, [activeEmbeddedStepKey, embedded, flattenedResolvedSteps]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embedded || !activeEmbeddedStepEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentStepComplete = isAgentWorkflowStepSelectionComplete({
|
||||
step: activeEmbeddedStepEntry.step,
|
||||
selectionValue: selections[activeEmbeddedStepEntry.step.key],
|
||||
});
|
||||
if (!currentStepComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStep = flattenedResolvedSteps
|
||||
.slice(activeEmbeddedStepIndex + 1)
|
||||
.find(
|
||||
({ step }) =>
|
||||
!isAgentWorkflowStepSelectionComplete({
|
||||
step,
|
||||
selectionValue: selections[step.key],
|
||||
})
|
||||
);
|
||||
|
||||
if (nextStep && nextStep.step.key !== activeEmbeddedStepKey) {
|
||||
setActiveEmbeddedStepKey(nextStep.step.key);
|
||||
}
|
||||
}, [
|
||||
activeEmbeddedStepEntry,
|
||||
activeEmbeddedStepIndex,
|
||||
activeEmbeddedStepKey,
|
||||
embedded,
|
||||
flattenedResolvedSteps,
|
||||
selections,
|
||||
]);
|
||||
|
||||
const renderOptionLevels = (
|
||||
step: AgentWorkflowUiConfig["steps"][number],
|
||||
@ -307,41 +374,8 @@ export function AgentWorkflowPanelDarkV2({
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-black">
|
||||
<AgentCanvasTopNavDark
|
||||
siteName={siteName}
|
||||
siteLogo={siteLogo}
|
||||
siteLogoDark={siteLogoDark}
|
||||
activeKey="agent"
|
||||
rightSlot={topNavRightSlot}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto overscroll-contain pt-16">
|
||||
{/* Hero Section */}
|
||||
<section className="relative">
|
||||
<div className="mx-auto max-w-[1480px] px-6 py-8 lg:px-8 xl:px-10">
|
||||
<AgentWorkflowShowcaseBanner
|
||||
config={showcaseConfig}
|
||||
actionLabel="开始选择风格"
|
||||
secondaryActionLabel={onSkipStep ? "进入风格生图" : undefined}
|
||||
onActionClick={() => {
|
||||
const target = document.getElementById("agent-workflow-content");
|
||||
if (!target) return;
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}}
|
||||
onSecondaryActionClick={onSkipStep}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="pb-40">
|
||||
<div
|
||||
id="agent-workflow-content"
|
||||
className="mx-auto max-w-[1480px] px-6 lg:px-8 xl:px-10"
|
||||
>
|
||||
<div className="space-y-8">
|
||||
const workflowContent = (
|
||||
<div className="space-y-8">
|
||||
{/* 基础配置 */}
|
||||
<motion.section
|
||||
className="rounded-2xl border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
||||
@ -392,34 +426,115 @@ export function AgentWorkflowPanelDarkV2({
|
||||
</motion.section>
|
||||
|
||||
{/* 工作流步骤 */}
|
||||
{flattenedResolvedSteps.map(({ step, depth }, stepIndex) => (
|
||||
<motion.section
|
||||
key={step.key}
|
||||
className={cn(
|
||||
"rounded-2xl border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]",
|
||||
depth > 1 && "ml-3 border-l-4 border-l-emerald-500/30 sm:ml-6"
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.15 + stepIndex * 0.05 }}
|
||||
>
|
||||
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-white">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-emerald-500/20 text-xs font-bold text-emerald-400">
|
||||
{stepIndex + 2}
|
||||
</span>
|
||||
{step.title}
|
||||
{depth > 1 ? (
|
||||
<span className="rounded-full border border-white/12 px-2 py-0.5 text-[11px] font-medium text-white/45">
|
||||
{depth === 2 ? "二级模块" : "三级模块"}
|
||||
{embedded ? (
|
||||
activeEmbeddedStepEntry ? (
|
||||
<motion.section
|
||||
key={activeEmbeddedStepEntry.step.key}
|
||||
className={cn(
|
||||
"rounded-2xl border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]",
|
||||
activeEmbeddedStepEntry.depth > 1 &&
|
||||
"border-l-4 border-l-emerald-500/30"
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<div className="mb-5 space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{flattenedResolvedSteps.map(({ step }, stepIndex) => {
|
||||
const completed = isAgentWorkflowStepSelectionComplete({
|
||||
step,
|
||||
selectionValue: selections[step.key],
|
||||
});
|
||||
const active = step.key === activeEmbeddedStepEntry.step.key;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={step.key}
|
||||
type="button"
|
||||
onClick={() => setActiveEmbeddedStepKey(step.key)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-colors",
|
||||
active
|
||||
? "border-emerald-400/45 bg-emerald-500/[0.12] text-emerald-100"
|
||||
: completed
|
||||
? "border-white/14 bg-white/[0.05] text-white/72 hover:border-white/22"
|
||||
: "border-white/10 bg-black/20 text-white/40 hover:border-white/16 hover:text-white/72"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-semibold",
|
||||
active
|
||||
? "bg-emerald-300 text-black"
|
||||
: completed
|
||||
? "bg-white text-black"
|
||||
: "bg-white/10 text-white/55"
|
||||
)}
|
||||
>
|
||||
{stepIndex + 1}
|
||||
</span>
|
||||
<span>{step.title}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex items-center gap-2 text-lg font-semibold text-white">
|
||||
<span className="flex h-7 w-7 items-center justify-center rounded-md bg-emerald-500/20 text-xs font-bold text-emerald-400">
|
||||
{activeEmbeddedStepIndex + 2}
|
||||
</span>
|
||||
{activeEmbeddedStepEntry.step.title}
|
||||
{activeEmbeddedStepEntry.depth > 1 ? (
|
||||
<span className="rounded-full border border-white/12 px-2 py-0.5 text-[11px] font-medium text-white/45">
|
||||
{activeEmbeddedStepEntry.depth === 2 ? "二级模块" : "三级模块"}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{activeEmbeddedStepEntry.step.description ? (
|
||||
<p className="text-sm text-white/40">
|
||||
{activeEmbeddedStepEntry.step.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renderOptionLevels(
|
||||
activeEmbeddedStepEntry.step,
|
||||
getEnabledAgentWorkflowOptionTree(activeEmbeddedStepEntry.step.options || [])
|
||||
)}
|
||||
</motion.section>
|
||||
) : null
|
||||
) : (
|
||||
flattenedResolvedSteps.map(({ step, depth }, stepIndex) => (
|
||||
<motion.section
|
||||
key={step.key}
|
||||
className={cn(
|
||||
"rounded-2xl border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]",
|
||||
depth > 1 && "ml-3 border-l-4 border-l-emerald-500/30 sm:ml-6"
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.15 + stepIndex * 0.05 }}
|
||||
>
|
||||
<h2 className="mb-4 flex items-center gap-2 text-lg font-semibold text-white">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-emerald-500/20 text-xs font-bold text-emerald-400">
|
||||
{stepIndex + 2}
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
{step.description && (
|
||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||
)}
|
||||
{renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))}
|
||||
</motion.section>
|
||||
))}
|
||||
{step.title}
|
||||
{depth > 1 ? (
|
||||
<span className="rounded-full border border-white/12 px-2 py-0.5 text-[11px] font-medium text-white/45">
|
||||
{depth === 2 ? "二级模块" : "三级模块"}
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
{step.description && (
|
||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||
)}
|
||||
{renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))}
|
||||
</motion.section>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* 当前选择摘要 */}
|
||||
{summary.length > 0 && (
|
||||
@ -470,6 +585,47 @@ export function AgentWorkflowPanelDarkV2({
|
||||
/>
|
||||
</motion.section>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return workflowContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-black">
|
||||
<AgentCanvasTopNavDark
|
||||
siteName={siteName}
|
||||
siteLogo={siteLogo}
|
||||
siteLogoDark={siteLogoDark}
|
||||
activeKey="agent"
|
||||
rightSlot={topNavRightSlot}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto overscroll-contain pt-16">
|
||||
{/* Hero Section */}
|
||||
<section className="relative">
|
||||
<div className="mx-auto max-w-[1480px] px-6 py-8 lg:px-8 xl:px-10">
|
||||
<AgentWorkflowShowcaseBanner
|
||||
config={showcaseConfig}
|
||||
actionLabel="开始选择风格"
|
||||
secondaryActionLabel={onSkipStep ? "进入风格生图" : undefined}
|
||||
onActionClick={() => {
|
||||
const target = document.getElementById("agent-workflow-content");
|
||||
if (!target) return;
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}}
|
||||
onSecondaryActionClick={onSkipStep}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="pb-40">
|
||||
<div
|
||||
id="agent-workflow-content"
|
||||
className="mx-auto max-w-[1480px] px-6 lg:px-8 xl:px-10"
|
||||
>
|
||||
{workflowContent}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ export function AgentWorkflowShowcaseBanner({
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<AgentWorkflowParticleScatter className="top-0 h-full opacity-50" />
|
||||
<AgentWorkflowParticleScatter className="top-0 h-full opacity-20" />
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[34%] bg-gradient-to-t from-black via-black/28 to-transparent" />
|
||||
|
||||
<div className="relative z-10">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user