ai-chatbot/components/agents/AgentPresetCardDark.tsx
Your Name 2ae376bdfd
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: move brand setup to first step
2026-03-21 19:03:12 +08:00

377 lines
13 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 { motion } from "framer-motion";
import { Play, Copy, Pencil, Sparkles, Trash2, Video, Wand2, FileText, Clock, User } from "lucide-react";
import { cn } from "@/lib/utils";
import { getAgentPresetModeTypeFromDefaultParams } from "@/lib/agents/preset-mode-config";
import type { AgentPreset } from "@/types/agent";
interface AgentPresetCardDarkProps {
preset: AgentPreset;
selected?: boolean;
onSelect: (presetId: string) => void;
onFork?: (preset: AgentPreset) => void;
onEdit?: (preset: AgentPreset) => void;
onDelete?: (preset: AgentPreset) => void;
index?: number;
compact?: boolean;
}
export function AgentPresetCardDark({
preset,
selected = false,
onSelect,
onFork,
onEdit,
onDelete,
index = 0,
compact = false,
}: AgentPresetCardDarkProps) {
const previewUrl =
preset.avatarUrl ||
preset.referenceAssets?.[0]?.thumbnailUrl ||
preset.referenceAssets?.[0]?.originalUrl ||
"";
const ownerName =
String(preset.owner?.name || "").trim() ||
String(preset.owner?.email || "").trim() ||
"未知";
const publishedAt = (() => {
const raw = preset.createdAt ? new Date(preset.createdAt) : null;
if (!raw || Number.isNaN(raw.getTime())) return "";
return raw.toLocaleDateString("zh-CN", {
month: "short",
day: "numeric",
});
})();
const typeConfig = {
image: {
icon: Wand2,
label: "生图",
gradient: "from-violet-500 to-purple-600",
bg: "bg-violet-500/20",
text: "text-violet-300"
},
video: {
icon: Video,
label: "视频",
gradient: "from-amber-500 to-orange-600",
bg: "bg-amber-500/20",
text: "text-amber-300"
},
text: {
icon: FileText,
label: "生文",
gradient: "from-emerald-500 to-teal-600",
bg: "bg-emerald-500/20",
text: "text-emerald-300"
},
};
const typeInfo = typeConfig[preset.type] || typeConfig.image;
const TypeIcon = typeInfo.icon;
const imageModeType = getAgentPresetModeTypeFromDefaultParams(preset.defaultParams);
const imageModeLabel =
imageModeType === "detail_replacement"
? "详情替换"
: imageModeType === "sku_replacement"
? "SKU替换"
: imageModeType === "scene_generation"
? "场景图"
: null;
const showEdit = Boolean(onEdit);
const showDelete = Boolean(onDelete);
const showFork = Boolean(onFork) && !showEdit && !showDelete;
if (compact) {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.35,
delay: index * 0.03,
ease: [0.25, 0.46, 0.45, 0.94],
}}
whileHover={{ y: -3, scale: 1.01 }}
whileTap={{ scale: 0.98 }}
className={cn(
"group relative cursor-pointer overflow-hidden rounded-2xl border p-3 transition-all duration-300",
selected
? "border-emerald-400/40 bg-emerald-500/[0.08] shadow-[0_0_24px_rgba(16,185,129,0.14)]"
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.06]"
)}
onClick={() => onSelect(preset.id)}
>
<div className="absolute right-2 top-2 z-10 flex gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
{showEdit ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onEdit?.(preset);
}}
className="flex h-6 w-6 items-center justify-center rounded-full bg-black/65 text-white/80 backdrop-blur-md transition-all hover:bg-white/20 hover:text-white"
>
<Pencil className="h-3 w-3" />
</button>
) : null}
{showDelete ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onDelete?.(preset);
}}
className="flex h-6 w-6 items-center justify-center rounded-full bg-black/65 text-white/80 backdrop-blur-md transition-all hover:bg-red-500/80 hover:text-white"
>
<Trash2 className="h-3 w-3" />
</button>
) : null}
{showFork ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onFork?.(preset);
}}
className="flex h-6 w-6 items-center justify-center rounded-full bg-black/65 text-white/80 backdrop-blur-md transition-all hover:bg-white/20 hover:text-white"
>
<Copy className="h-3 w-3" />
</button>
) : null}
</div>
<div className="flex items-center gap-3">
<div className="relative h-[76px] w-[76px] shrink-0 overflow-hidden rounded-2xl border border-white/10 bg-black/30">
{previewUrl ? (
<img
src={previewUrl}
alt={preset.name}
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className={cn("flex h-full w-full items-center justify-center bg-gradient-to-br", typeInfo.gradient)}>
<TypeIcon className="h-7 w-7 text-white" />
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/35 to-transparent" />
<div className="absolute left-2 top-2 rounded-full border border-white/10 bg-black/45 px-2 py-0.5 text-[10px] text-white/78">
{imageModeLabel || typeInfo.label}
</div>
</div>
<div className="min-w-0 flex-1 pr-8">
<div className="line-clamp-2 text-sm font-semibold leading-5 text-white">
{preset.name}
</div>
<div className="mt-2 flex flex-col gap-1.5 text-[11px] text-white/50">
<div className="flex items-center gap-1.5">
<User className="h-3.5 w-3.5 text-white/34" />
<span className="truncate">贡献者:{ownerName}</span>
</div>
<div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 text-white/34" />
<span className="truncate">贡献时间:{publishedAt || "未记录"}</span>
</div>
</div>
</div>
</div>
{selected ? (
<div className="pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-emerald-300/40" />
) : null}
</motion.div>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.5,
delay: index * 0.08,
ease: [0.25, 0.46, 0.45, 0.94]
}}
whileHover={{ y: -8, scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={cn(
"group relative cursor-pointer",
"w-full aspect-[4/3]",
"rounded-2xl overflow-hidden",
"bg-zinc-900",
"transition-all duration-500",
selected
? "ring-2 ring-white/30 shadow-[0_0_40px_rgba(255,255,255,0.1)]"
: "ring-1 ring-white/5 hover:ring-white/20"
)}
onClick={() => onSelect(preset.id)}
>
{/* 背景图片 */}
<div className="absolute inset-0">
{previewUrl ? (
<>
<img
src={previewUrl}
alt={preset.name}
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/90 via-black/40 to-black/10" />
<div className="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-transparent" />
</>
) : (
<div className="h-full w-full bg-gradient-to-br from-zinc-800 to-zinc-900">
<div className="flex h-full items-center justify-center">
<div className={cn(
"flex h-20 w-20 items-center justify-center rounded-3xl",
"bg-gradient-to-br",
typeInfo.gradient,
"opacity-60"
)}>
<TypeIcon className="h-10 w-10 text-white" />
</div>
</div>
</div>
)}
</div>
{/* 顶部信息栏 */}
<div className="absolute left-0 right-0 top-0 flex items-center justify-between p-4">
{/* 类型标签 */}
<div className={cn(
"flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium backdrop-blur-md",
"bg-black/40",
typeInfo.text
)}>
<TypeIcon className="h-3.5 w-3.5" />
{imageModeLabel || typeInfo.label}
</div>
{/* 可见性标签 */}
<div className={cn(
"rounded-full px-2.5 py-1 text-[10px] font-medium backdrop-blur-md",
preset.visibility === "public"
? "bg-emerald-500/20 text-emerald-300"
: "bg-zinc-500/20 text-zinc-400"
)}>
{preset.visibility === "public" ? "公共" : "私有"}
</div>
</div>
{/* 中央播放/使用按钮 */}
<motion.div
className="absolute inset-0 flex items-center justify-center"
initial={{ opacity: 0, scale: 0.8 }}
whileHover={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
<div className={cn(
"flex h-16 w-16 items-center justify-center rounded-full",
"bg-white/20 backdrop-blur-md",
"ring-1 ring-white/30",
"transition-all duration-300",
"group-hover:bg-white/30 group-hover:scale-110",
selected && "bg-white/40 scale-110"
)}>
<Play className={cn(
"h-7 w-7 text-white fill-white",
selected && "scale-110"
)} />
</div>
</motion.div>
{/* 悬浮操作按钮 */}
<motion.div
className="absolute right-4 top-16 flex flex-col gap-2"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
>
<div className="flex flex-col gap-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
{showEdit && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onEdit?.(preset);
}}
className="flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white/80 backdrop-blur-md transition-all hover:bg-white/20 hover:text-white"
>
<Pencil className="h-4 w-4" />
</button>
)}
{showDelete && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onDelete?.(preset);
}}
className="flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white/80 backdrop-blur-md transition-all hover:bg-red-500/80 hover:text-white"
>
<Trash2 className="h-4 w-4" />
</button>
)}
{showFork && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onFork?.(preset);
}}
className="flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white/80 backdrop-blur-md transition-all hover:bg-white/20 hover:text-white"
>
<Copy className="h-4 w-4" />
</button>
)}
</div>
</motion.div>
{/* 底部信息 */}
<div className="absolute bottom-0 left-0 right-0 p-4">
{/* 标题 */}
<h3 className="mb-2 line-clamp-1 text-base font-semibold text-white drop-shadow-lg">
{preset.name}
</h3>
{/* 元信息 */}
<div className="flex items-center justify-between text-xs text-white/60">
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
<User className="h-3 w-3" />
{ownerName}
</span>
{preset.slotCount > 0 && (
<span className="flex items-center gap-1">
<Sparkles className="h-3 w-3" />
{preset.slotCount}槽
</span>
)}
</div>
{publishedAt && (
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{publishedAt}
</span>
)}
</div>
</div>
{/* 选中指示器 */}
{selected && (
<motion.div
className="absolute inset-0 ring-2 ring-inset ring-white/30 rounded-2xl"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
)}
</motion.div>
);
}