ai-chatbot/components/agents/AgentPresetCardDark.tsx
Your Name 87a6e6561d
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: compact embedded agent preset cards
2026-03-19 13:18:10 +08:00

321 lines
11 KiB
TypeScript

"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;
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-2.5 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="relative mx-auto h-[70px] w-[70px] 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>
<div className="mt-2 space-y-1 text-center">
<div className="line-clamp-2 min-h-[2.5rem] text-[12px] font-medium leading-5 text-white">
{preset.name}
</div>
<div className="text-[10px] text-white/45">{imageModeLabel || typeInfo.label}</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">
{preset.visibility === "private" && onEdit && (
<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>
)}
{preset.visibility === "private" && onDelete && (
<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>
)}
{preset.visibility === "public" && onFork && (
<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>
);
}