ai-chatbot/components/agents/AgentWorkflowPanelDarkV2.tsx
Your Name f764767ba4
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: refresh agent workflow dark ui
2026-03-18 00:24:32 +08:00

512 lines
19 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 { useMemo } from "react";
import { motion } from "framer-motion";
import { ArrowRight, Check, Copy, RotateCcw, Sparkles, ChevronRight, Wand2, LayoutGrid, FolderOpen, User, Building2, Menu } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
import {
buildAgentWorkflowComposerContext,
getAgentWorkflowProgress,
} from "@/lib/agents/workflow-composer";
import type {
AgentWorkflowComposerSelection,
AgentWorkflowMaterialSelections,
AgentWorkflowStepKey,
AgentWorkflowUiConfig,
} from "@/types/agent";
export interface AgentWorkflowInputs {
primarySku: string;
secondarySku: string;
brand: string;
store: 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;
}
function buildSelectionSummary(params: {
config: AgentWorkflowUiConfig;
selections: Partial<Record<AgentWorkflowStepKey, string>>;
}) {
const result: AgentWorkflowComposerSelection[] = [];
for (const step of params.config.steps) {
const selectedOptionKey = params.selections[step.key];
if (!selectedOptionKey) continue;
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
if (!option) continue;
result.push({
stepKey: step.key,
stepTitle: step.title,
optionKey: option.key,
optionName: option.buttonName,
promptFragment: option.promptFragment || "",
});
}
return result;
}
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/5 hover:ring-white/20"
)}
>
{/* 背景 */}
<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 AgentWorkflowPanelDarkV2({
config,
inputs,
onInputsChange,
materials,
onMaterialsChange,
selections,
onSelectionsChange,
onReset,
onNextStep,
onSkipStep,
}: AgentWorkflowPanelDarkV2Props) {
const brandOptions = useMemo(
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
[config?.inputOptions?.brands]
);
const storeOptions = useMemo(
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
[config?.inputOptions?.stores]
);
const resolvedConfig = useMemo(() => {
const source = config || defaultAgentWorkflowUiConfig;
return {
steps: [...source.steps]
.filter((step) => step.enabled)
.map((step) => ({
...step,
options: [...step.options]
.filter((option) => option.enabled)
.sort((left, right) => left.sortOrder - right.sortOrder),
}))
.filter((step) => step.options.length > 0)
.sort((left, right) => left.sortOrder - right.sortOrder),
};
}, [config]);
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 handleCopyPrompt = async () => {
if (!composerContext?.promptText) {
toast.error("当前还没有可复制的 Prompt");
return;
}
try {
await navigator.clipboard.writeText(composerContext.promptText);
toast.success("Prompt 已复制");
} catch {
toast.error("复制失败");
}
};
// 导航标签配置
const navTabs = [
{ id: "cases", label: "案例广场", icon: LayoutGrid },
{ id: "my-works", label: "我的作品", icon: FolderOpen },
{ id: "personal-assets", label: "个人资产库", icon: User },
{ id: "company-assets", label: "公司资产库", icon: Building2 },
];
return (
<div className="min-h-screen bg-black">
{/* 顶部固定导航栏 */}
<header className="fixed top-0 left-0 right-0 z-50 border-b border-white/5 bg-black/80 backdrop-blur-xl">
<div className="mx-auto max-w-[1800px] px-6">
<div className="flex h-16 items-center justify-between">
{/* Logo & Site Name */}
<div className="flex items-center gap-3">
<button className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/5 text-white/60 hover:bg-white/10 hover:text-white transition-colors">
<Menu className="h-5 w-5" />
</button>
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-purple-600">
<Wand2 className="h-4 w-4 text-white" />
</div>
<span className="text-base font-semibold text-white">AI创作平台</span>
</div>
</div>
{/* 导航标签 */}
<nav className="flex items-center gap-1 rounded-full bg-white/5 p-1">
{navTabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
className="flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium text-white/60 transition-all duration-300 hover:text-white hover:bg-white/5"
>
<Icon className="h-4 w-4" />
{tab.label}
</button>
);
})}
</nav>
{/* 右侧操作 */}
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="sm"
onClick={() => {
if (onReset) {
onReset();
return;
}
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "" });
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
secondary: createEmptyAgentWorkflowMaterialSelection(),
});
onSelectionsChange({});
}}
className="h-9 gap-1.5 rounded-full text-white/60 hover:bg-white/10 hover:text-white"
>
<RotateCcw className="h-3.5 w-3.5" />
重置
</Button>
</div>
</div>
</div>
</header>
{/* Hero Section */}
<section className="relative pt-16">
<div className="mx-auto max-w-[1800px] px-6 py-8">
{/* 标题区域 */}
<motion.div
className="mb-8 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="mb-3 text-3xl font-bold text-white md:text-4xl">
三倍速出片,电影级质感
</h1>
<p className="mb-6 text-base text-white/50">
<span className="text-emerald-400 font-medium">第一步:风格选择</span>
<span className="mx-2 text-white/20">→</span>
<span className="text-white/40">第二步:智能体生图</span>
</p>
</motion.div>
{/* 进度指示 */}
<motion.div
className="mx-auto mb-8 max-w-md"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
<div className="flex items-center justify-between text-sm text-white/50 mb-2">
<span>完成进度</span>
<span className="font-medium text-white">{progress.completed}/{progress.total}</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-white/10">
<motion.div
className="h-full rounded-full bg-gradient-to-r from-emerald-500 to-teal-500"
initial={{ width: 0 }}
animate={{ width: `${progress.total > 0 ? (progress.completed / progress.total) * 100 : 0}%` }}
transition={{ duration: 0.5 }}
/>
</div>
</motion.div>
</div>
</section>
{/* Main Content */}
<main className="pb-20">
<div className="mx-auto max-w-[1800px] px-6">
<ScrollArea className="h-[calc(100vh-300px)]">
<div className="space-y-8 pr-4">
{/* 基础配置 */}
<motion.section
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>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 max-w-2xl">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
placeholder="选择品牌"
searchPlaceholder="输入品牌名筛选"
emptyText="暂无品牌可选"
onValueChange={(value) => onInputsChange({ ...inputs, brand: value })}
/>
<SearchableSelect
value={inputs.store}
options={storeOptions}
placeholder="选择店铺"
searchPlaceholder="输入店铺名筛选"
emptyText="暂无店铺可选"
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
/>
</div>
</motion.section>
{/* 工作流步骤 */}
{resolvedConfig.steps.map((step, stepIndex) => (
<motion.section
key={step.key}
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}
</h2>
{step.description && (
<p className="mb-4 text-sm text-white/40">{step.description}</p>
)}
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{step.options.map((option, optionIndex) => (
<StepOptionCard
key={option.key}
option={option}
step={step}
selected={selections[step.key] === option.key}
onClick={() =>
onSelectionsChange({
...selections,
[step.key]: selections[step.key] === option.key ? undefined : option.key,
})
}
index={optionIndex}
/>
))}
</div>
</motion.section>
))}
{/* 当前选择摘要 */}
{summary.length > 0 && (
<motion.section
className="rounded-2xl border border-white/5 bg-white/[0.02] p-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<h3 className="mb-4 text-sm font-semibold text-white/80">当前选择</h3>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
{summary.map((item) => (
<div
key={`${item.stepKey}-${item.optionKey}`}
className="flex items-center justify-between rounded-xl bg-white/5 px-4 py-3"
>
<span className="text-sm text-white/50">{item.stepTitle}</span>
<span className="text-sm font-medium text-white">{item.optionName}</span>
</div>
))}
</div>
</motion.section>
)}
{/* Prompt 预览 */}
<motion.section
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/5 bg-white/[0.03] text-sm text-white/80 placeholder:text-white/20 focus:border-white/10"
/>
</motion.section>
{/* 底部操作 */}
{onNextStep && (
<motion.div
className="flex justify-end gap-3 pt-4 pb-8"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{onSkipStep && (
<Button
variant="ghost"
onClick={onSkipStep}
className="h-11 gap-2 rounded-full text-white/60 hover:bg-white/10 hover:text-white"
>
跳过
<ChevronRight className="h-4 w-4" />
</Button>
)}
<Button
onClick={onNextStep}
disabled={progress.total > 0 && !progress.isComplete}
className={cn(
"h-11 gap-2 rounded-full px-8",
"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_30px_rgba(16,185,129,0.3)]",
"disabled:opacity-30"
)}
>
下一步:选择智能体
<ArrowRight className="h-4 w-4" />
</Button>
</motion.div>
)}
</div>
</ScrollArea>
</div>
</main>
{/* 页脚 */}
<footer className="border-t border-white/5 py-6">
<div className="mx-auto max-w-[1800px] px-6">
<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>
);
}