feat: unify agent workflow into anchored single page
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 12:17:16 +08:00
parent 44b2148bef
commit 9631c10812
4 changed files with 640 additions and 308 deletions

View File

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

View File

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

View File

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

View File

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