804 lines
30 KiB
TypeScript
804 lines
30 KiB
TypeScript
"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>
|
||
);
|
||
}
|