ai-chatbot/components/agents/AgentWorkflowPanelDarkV2.tsx
Your Name ccfaef9df9
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add detail reference stitch workflow
2026-03-23 17:36:55 +08:00

804 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { Fragment, 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";
import { Button } from "@/components/ui/button";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
flattenAgentWorkflowSteps,
flattenVisibleAgentWorkflowSteps,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowSelectedOptionPath,
getAgentWorkflowStoreOptionsByBrand,
getEnabledAgentWorkflowOptionTree,
getEnabledAgentWorkflowStepTree,
isAgentWorkflowStepSelectionComplete,
serializeAgentWorkflowSelectionPath,
} from "@/lib/agents/workflow-ui-config";
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner";
import {
buildAgentWorkflowComposerContext,
getAgentWorkflowProgress,
} from "@/lib/agents/workflow-composer";
import type {
AgentWorkflowComposerSelection,
AgentWorkflowMaterialSelections,
AgentWorkflowShowcaseConfig,
AgentWorkflowStepKey,
AgentWorkflowUiConfig,
} from "@/types/agent";
export interface AgentWorkflowInputs {
primarySku: string;
secondarySku: string;
brand: string;
store: string;
priceBand: string;
}
interface AgentWorkflowPanelDarkV2Props {
config?: AgentWorkflowUiConfig | null;
inputs: AgentWorkflowInputs;
onInputsChange: (next: AgentWorkflowInputs) => void;
materials: AgentWorkflowMaterialSelections;
onMaterialsChange: (next: AgentWorkflowMaterialSelections) => void;
selections: Partial<Record<AgentWorkflowStepKey, string>>;
onSelectionsChange: (next: Partial<Record<AgentWorkflowStepKey, string>>) => void;
onReset?: () => void;
onNextStep?: () => void;
onSkipStep?: () => void;
siteName?: string | null;
siteLogo?: string | null;
siteLogoDark?: string | null;
showcaseConfig?: AgentWorkflowShowcaseConfig | null;
topNavRightSlot?: ReactNode;
embedded?: boolean;
showBrandStoreSection?: boolean;
}
interface AgentWorkflowBrandStorePanelProps {
config?: AgentWorkflowUiConfig | null;
inputs: AgentWorkflowInputs;
onInputsChange: (next: AgentWorkflowInputs) => void;
theme?: "light" | "dark";
title?: string;
description?: string;
}
function buildSelectionSummary(params: {
config: AgentWorkflowUiConfig;
selections: Partial<Record<AgentWorkflowStepKey, string>>;
}) {
const result: AgentWorkflowComposerSelection[] = [];
for (const { step } of flattenVisibleAgentWorkflowSteps(params.config.steps, params.selections)) {
const selectedOptions = getAgentWorkflowSelectedOptionPath({
step,
selectionValue: params.selections[step.key],
});
selectedOptions.forEach((option) => {
result.push({
stepKey: step.key,
stepTitle: step.title,
optionKey: option.key,
optionName: option.buttonName,
promptFragment: option.promptFragment || "",
});
});
}
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,
selected,
onClick,
index,
}: {
option: { key: string; buttonName: string; description?: string | null; previewImageUrl?: string | null };
step: { title: string };
selected: boolean;
onClick: () => void;
index: number;
}) {
return (
<motion.button
type="button"
onClick={onClick}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: index * 0.05 }}
whileHover={{ y: -4, scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={cn(
"group relative overflow-hidden rounded-2xl text-left",
"aspect-[16/10] w-full",
"transition-all duration-500",
selected
? "ring-2 ring-white/30 shadow-[0_0_30px_rgba(255,255,255,0.1)]"
: "ring-1 ring-white/12 hover:ring-white/24"
)}
>
{/* 背景 */}
<div className="absolute inset-0 bg-zinc-900">
{option.previewImageUrl ? (
<>
<img
src={option.previewImageUrl}
alt={option.buttonName}
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent" />
</>
) : (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-zinc-800 to-zinc-900">
<Sparkles className="h-10 w-10 text-white/20" />
</div>
)}
</div>
{/* 选中标记 */}
{selected && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute right-3 top-3 flex h-6 w-6 items-center justify-center rounded-full bg-white text-black"
>
<Check className="h-4 w-4" />
</motion.div>
)}
{/* 步骤标签 */}
<div className="absolute left-3 top-3">
<span className="rounded-full bg-black/60 px-2.5 py-1 text-[10px] font-medium text-white/80 backdrop-blur-md">
{step.title}
</span>
</div>
{/* 底部信息 */}
<div className="absolute bottom-0 left-0 right-0 p-4">
<h4 className="text-sm font-semibold text-white drop-shadow-lg">
{option.buttonName}
</h4>
{option.description && (
<p className="mt-1 line-clamp-1 text-xs text-white/60">
{option.description}
</p>
)}
</div>
{/* 选中边框 */}
{selected && (
<div className="absolute inset-0 rounded-2xl ring-2 ring-inset ring-white/30" />
)}
</motion.button>
);
}
export function AgentWorkflowBrandStorePanel({
config,
inputs,
onInputsChange,
theme = "dark",
title = "品牌和店铺选择",
description = "先选择品牌、店铺和价格带,后续品牌模特与风格步骤会按这里的选择联动。",
}: AgentWorkflowBrandStorePanelProps) {
const brandOptions = useMemo(
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
[config?.inputOptions?.brands]
);
const brandStoreEntries = useMemo(
() => getAgentWorkflowBrandStoreEntries(config),
[config]
);
const storeOptions = useMemo(
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
);
const isDark = theme === "dark";
return (
<div
className={cn(
"rounded-[24px] border p-4",
isDark ? "border-white/12 bg-black/20" : "border-border bg-background"
)}
>
<div className="mb-4 space-y-1">
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>
{title}
</div>
<div className={cn("text-xs leading-5", isDark ? "text-white/48" : "text-muted-foreground")}>
{description}
</div>
</div>
<div className="grid max-w-5xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
placeholder="选择品牌"
searchPlaceholder="输入品牌名筛选"
emptyText="暂无品牌可选"
theme={theme}
onValueChange={(value) =>
onInputsChange({
...inputs,
brand: value,
store:
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.store
: "",
})
}
/>
<SearchableSelect
value={inputs.store}
options={storeOptions}
placeholder="选择店铺"
searchPlaceholder="输入店铺名筛选"
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
theme={theme}
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
/>
<SearchableSelect
value={inputs.priceBand}
options={priceBandOptions}
placeholder="选择价格带"
searchPlaceholder="输入价格带筛选"
emptyText="暂无价格带可选"
theme={theme}
disabled={priceBandOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, priceBand: value })}
/>
</div>
</div>
);
}
export function AgentWorkflowPanelDarkV2({
config,
inputs,
onInputsChange,
materials,
onMaterialsChange,
selections,
onSelectionsChange,
onNextStep,
onSkipStep,
siteName,
siteLogo,
siteLogoDark,
showcaseConfig,
topNavRightSlot,
embedded = false,
showBrandStoreSection = true,
}: AgentWorkflowPanelDarkV2Props) {
const resolvedConfig = useMemo(() => {
const source = config || defaultAgentWorkflowUiConfig;
return {
steps: getEnabledAgentWorkflowStepTree(source.steps || []),
};
}, [config]);
const flattenedResolvedSteps = useMemo(
() =>
flattenVisibleAgentWorkflowSteps(resolvedConfig.steps, selections).filter(
({ step }) => Array.isArray(step.options) && step.options.length > 0
),
[resolvedConfig.steps, selections]
);
const summary = useMemo(
() => buildSelectionSummary({ config: resolvedConfig, selections }),
[resolvedConfig, selections]
);
const composerContext = useMemo(
() =>
buildAgentWorkflowComposerContext({
config: resolvedConfig,
inputs,
materials,
selections,
}),
[inputs, materials, resolvedConfig, selections]
);
const progress = useMemo(
() => 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(
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;
}
if (pendingAutoAdvanceStepKey !== activeEmbeddedStepEntry.step.key) {
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);
}
setPendingAutoAdvanceStepKey(null);
}, [
activeEmbeddedStepEntry,
activeEmbeddedStepIndex,
activeEmbeddedStepKey,
embedded,
flattenedResolvedSteps,
pendingAutoAdvanceStepKey,
selections,
]);
const renderOptionLevels = (
step: AgentWorkflowUiConfig["steps"][number],
options: AgentWorkflowUiConfig["steps"][number]["options"],
depth = 1,
parentPath: string[] = []
): ReactNode => {
const selectedPathKeys = getAgentWorkflowSelectedOptionPath({
step,
selectionValue: selections[step.key],
}).map((option) => option.key);
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
const currentSelectedOption =
options.find((option) => option.key === currentSelectedKey) || null;
const visibleChildren = getEnabledAgentWorkflowOptionTree(
currentSelectedOption?.children || []
);
const levelLabel = depth === 1 ? "主卡片" : depth === 2 ? "二级卡片" : "三级卡片";
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<span className="rounded-full border border-white/12 px-2.5 py-1 text-[11px] font-medium text-white/45">
{levelLabel}
</span>
{depth > 1 ? (
<span className="text-xs text-white/35">
该层卡片从上一级已选卡片中继续展开
</span>
) : null}
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{options.map((option, optionIndex) => (
<StepOptionCard
key={`${option.key}-${depth}`}
option={option}
step={step}
selected={currentSelectedKey === option.key}
onClick={() => {
const prefix = parentPath.slice(0, depth - 1);
const nextKeys =
currentSelectedKey === option.key ? prefix : [...prefix, option.key];
setPendingAutoAdvanceStepKey(step.key);
onSelectionsChange({
...selections,
[step.key]: nextKeys.length > 0 ? serializeAgentWorkflowSelectionPath(nextKeys) : undefined,
});
}}
index={optionIndex}
/>
))}
</div>
{currentSelectedOption && visibleChildren.length > 0
? renderOptionLevels(step, visibleChildren, depth + 1, [
...parentPath.slice(0, depth - 1),
currentSelectedOption.key,
])
: null}
</div>
);
};
const handleCopyPrompt = async () => {
if (!composerContext?.promptText) {
toast.error("当前还没有可复制的 Prompt");
return;
}
try {
await navigator.clipboard.writeText(composerContext.promptText);
toast.success("Prompt 已复制");
} catch {
toast.error("复制失败");
}
};
const workflowContent = (
<div className="space-y-8">
{showBrandStoreSection ? (
<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)]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<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">
1
</span>
品牌和店铺选择
</h2>
<AgentWorkflowBrandStorePanel
config={config}
inputs={inputs}
onInputsChange={onInputsChange}
theme="dark"
/>
</motion.section>
) : null}
{/* 工作流步骤 */}
{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="mt-3">
<div
className="grid items-start gap-x-2 gap-y-3"
style={{
gridTemplateColumns: `repeat(${flattenedResolvedSteps.length}, minmax(0, 1fr))`,
}}
>
{flattenedResolvedSteps.map(({ step }, stepIndex) => {
const completed = isAgentWorkflowStepSelectionComplete({
step,
selectionValue: selections[step.key],
});
const active = step.key === activeEmbeddedStepEntry.step.key;
const stepSummary = stepSelectionSummaryMap[step.key] || "";
return (
<Fragment key={step.key}>
<button
type="button"
onClick={() => {
setPendingAutoAdvanceStepKey(null);
setActiveEmbeddedStepKey(step.key);
}}
className="group relative flex min-w-0 flex-col items-start pr-2 text-left transition-transform duration-200 hover:-translate-y-[1px]"
>
<div className="flex w-full items-center gap-3">
<span
className={cn(
"flex h-7 w-7 shrink-0 items-center justify-center rounded-full border text-[11px] font-semibold transition-colors",
active
? "border-emerald-300 bg-emerald-300 text-black shadow-[0_0_18px_rgba(52,211,153,0.28)]"
: completed
? "border-white/30 bg-white text-black"
: "border-white/12 bg-white/[0.04] text-white/62 group-hover:border-white/24 group-hover:text-white/82"
)}
>
{stepIndex + 1}
</span>
<span
className={cn(
"truncate text-[13px] font-semibold transition-colors",
active
? "text-white"
: completed
? "text-white/82"
: "text-white/52 group-hover:text-white/76"
)}
>
{step.title}
</span>
</div>
<span
className={cn(
"mt-1.5 pl-10 text-[10px] leading-4",
active
? "text-emerald-100/78"
: completed
? "text-white/50"
: "text-white/32"
)}
>
{stepSummary || "待选择"}
</span>
{stepIndex < flattenedResolvedSteps.length - 1 ? (
<div
className={cn(
"pointer-events-none absolute left-[calc(100%-4px)] top-3.5 h-px w-2",
stepIndex < activeEmbeddedStepIndex
? "bg-emerald-400/35"
: "bg-white/10"
)}
/>
) : null}
</button>
</Fragment>
);
})}
</div>
</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>
{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>
<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>
)}
{renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))}
</motion.section>
))
)}
{/* Prompt 预览 */}
<motion.section
className="rounded-2xl border border-white/10 bg-white/[0.035] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-sm font-semibold text-white/80">Prompt 预览</h3>
<Button
variant="ghost"
size="sm"
onClick={handleCopyPrompt}
className="h-8 gap-1.5 text-xs text-white/60 hover:bg-white/10 hover:text-white"
>
<Copy className="h-3.5 w-3.5" />
复制
</Button>
</div>
<Textarea
value={composerContext?.promptText || ""}
readOnly
rows={6}
placeholder="选择工作流后,这里会自动生成结构化 Prompt"
className="resize-none rounded-xl border-white/12 bg-black/20 text-sm text-white/80 placeholder:text-white/20 focus:border-white/15"
/>
</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>
{onNextStep ? (
<motion.div
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mx-auto mt-8 w-full max-w-[1480px] px-6 lg:px-8 xl:px-10"
>
<div className="flex w-full flex-col gap-3 rounded-[24px] border border-white/12 bg-black/88 px-3 py-3 shadow-[0_18px_54px_rgba(0,0,0,0.42)] backdrop-blur-2xl sm:px-4">
<div className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<div className="text-[13px] font-medium text-white">工作流进度</div>
<div className="text-[11px] text-white/45">
已完成 {progress.completed} / {progress.total} 个步骤
{progress.isComplete ? ",可以直接进入下一步" : ",也可以选择跳过直接进入下一步"}
</div>
</div>
<div className="text-[13px] font-semibold text-white">
{progress.total > 0 ? Math.round((progress.completed / progress.total) * 100) : 0}%
</div>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-white/10">
<motion.div
className="h-full rounded-full bg-gradient-to-r from-emerald-500 to-teal-400"
initial={{ width: 0 }}
animate={{ width: `${progress.total > 0 ? (progress.completed / progress.total) * 100 : 0}%` }}
transition={{ duration: 0.45 }}
/>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
{onSkipStep ? (
<Button
variant="ghost"
onClick={onSkipStep}
className="h-10 gap-2 rounded-full border border-white/10 bg-white/[0.03] px-5 text-white/75 hover:bg-white/10 hover:text-white"
>
跳过并进入下一步
<ChevronRight className="h-4 w-4" />
</Button>
) : null}
<Button
onClick={onNextStep}
disabled={progress.total > 0 && !progress.isComplete}
className={cn(
"h-10 gap-2 rounded-full px-6",
"bg-gradient-to-r from-emerald-500 to-teal-500",
"text-sm font-medium text-white",
"hover:from-emerald-400 hover:to-teal-400",
"shadow-[0_0_30px_rgba(16,185,129,0.3)]",
"disabled:opacity-30"
)}
>
下一步:选择生图模式
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
</motion.div>
) : null}
{/* 页脚 */}
<footer className="mt-8 border-t border-white/5 pb-8 pt-6">
<div className="mx-auto max-w-[1480px] px-6 lg:px-8 xl:px-10">
<div className="flex items-center justify-center gap-6 text-sm text-white/30">
<a href="#" className="hover:text-white/60 transition-colors">使用协议</a>
<a href="#" className="hover:text-white/60 transition-colors">隐私条款</a>
<span>© 2024 AI创作平台</span>
</div>
</div>
</footer>
</div>
</div>
);
}