ai-chatbot/components/agents/AgentPresetCardNew.tsx
Your Name 8757f76b1f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
chore: rename agent UI to stylized
2026-04-11 19:48:45 +08:00

251 lines
9.7 KiB
TypeScript

"use client";
import { motion } from "framer-motion";
import { Copy, Pencil, Sparkles, Trash2, Video, Wand2, FileText } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import type { AgentPreset } from "@/types/agent";
interface AgentPresetCardNewProps {
preset: AgentPreset;
selected?: boolean;
onSelect: (presetId: string) => void;
onFork?: (preset: AgentPreset) => void;
onEdit?: (preset: AgentPreset) => void;
onDelete?: (preset: AgentPreset) => void;
index?: number;
}
export function AgentPresetCardNew({
preset,
selected = false,
onSelect,
onFork,
onEdit,
onDelete,
index = 0,
}: AgentPresetCardNewProps) {
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", {
timeZone: "Asia/Shanghai",
year: "numeric",
month: "2-digit",
day: "2-digit",
});
})();
const typeConfig = {
image: { icon: Wand2, label: "生图", color: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400" },
video: { icon: Video, label: "视频", color: "bg-amber-500/10 text-amber-600 dark:text-amber-400" },
text: { icon: FileText, label: "生文", color: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" },
};
const typeInfo = typeConfig[preset.type] || typeConfig.image;
const TypeIcon = typeInfo.icon;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.4,
delay: index * 0.05,
ease: [0.25, 0.46, 0.45, 0.94]
}}
whileHover={{ y: -4 }}
className={cn(
"group relative overflow-hidden rounded-2xl bg-white transition-all duration-300",
"border border-slate-200/60 dark:border-slate-800/60",
"shadow-[0_0_0_1px_rgba(0,0,0,0.02),0_2px_8px_rgba(0,0,0,0.04)]",
"hover:shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_12px_32px_rgba(0,0,0,0.08)]",
"hover:border-indigo-200/60 dark:hover:border-indigo-800/40",
selected && [
"border-indigo-500/50 dark:border-indigo-400/50",
"shadow-[0_0_0_1px_rgba(99,102,241,0.2),0_8px_24px_rgba(99,102,241,0.12)]",
"ring-1 ring-indigo-500/20",
]
)}
>
{/* 顶部选择指示条 */}
<div
className={cn(
"absolute left-0 right-0 top-0 h-[3px] transition-all duration-300",
selected
? "bg-gradient-to-r from-indigo-500 via-violet-500 to-indigo-500"
: "bg-transparent group-hover:bg-gradient-to-r group-hover:from-indigo-500/30 group-hover:via-violet-500/30 group-hover:to-indigo-500/30"
)}
/>
<button
type="button"
className="block w-full text-left"
onClick={() => onSelect(preset.id)}
>
{/* 预览图区域 */}
<div className="relative aspect-[4/3] w-full overflow-hidden bg-slate-50 dark:bg-slate-900/50">
{previewUrl ? (
<motion.img
src={previewUrl}
alt={preset.name}
className="h-full w-full object-cover"
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94] }}
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
<div className={cn(
"flex h-16 w-16 items-center justify-center rounded-2xl",
typeInfo.color
)}>
<TypeIcon className="h-7 w-7" />
</div>
</div>
)}
{/* 类型标签 */}
<div className="absolute left-3 top-3">
<Badge
variant="secondary"
className={cn(
"border-0 px-2.5 py-1 text-[11px] font-medium backdrop-blur-md",
"bg-white/85 text-slate-700 shadow-sm",
"dark:bg-slate-950/80 dark:text-slate-200"
)}
>
<TypeIcon className="mr-1 h-3 w-3" />
{typeInfo.label}
</Badge>
</div>
{/* 可见性标签 */}
<div className="absolute right-3 top-3">
<Badge
variant="secondary"
className={cn(
"border-0 px-2 py-0.5 text-[10px] font-medium backdrop-blur-md",
preset.visibility === "public"
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400"
: "bg-slate-500/10 text-slate-700 dark:text-slate-400"
)}
>
{preset.visibility === "public" ? "公共" : "私有"}
</Badge>
</div>
{/* 悬浮操作按钮 */}
<motion.div
className="absolute inset-x-3 bottom-3 flex items-center justify-end gap-1.5"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 + 0.2 }}
>
<div className="flex gap-1.5 opacity-0 transition-all duration-200 group-hover:opacity-100">
{preset.visibility === "private" && onEdit && (
<motion.button
type="button"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="flex h-8 w-8 items-center justify-center rounded-full bg-white/95 text-slate-700 shadow-lg backdrop-blur-sm transition-colors hover:bg-white hover:text-indigo-600 dark:bg-slate-900/95 dark:text-slate-300 dark:hover:text-indigo-400"
onClick={(event) => {
event.stopPropagation();
onEdit(preset);
}}
title="编辑"
>
<Pencil className="h-3.5 w-3.5" />
</motion.button>
)}
{preset.visibility === "private" && onDelete && (
<motion.button
type="button"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="flex h-8 w-8 items-center justify-center rounded-full bg-white/95 text-slate-700 shadow-lg backdrop-blur-sm transition-colors hover:bg-white hover:text-rose-600 dark:bg-slate-900/95 dark:text-slate-300 dark:hover:text-rose-400"
onClick={(event) => {
event.stopPropagation();
onDelete(preset);
}}
title="删除"
>
<Trash2 className="h-3.5 w-3.5" />
</motion.button>
)}
{preset.visibility === "public" && onFork && (
<motion.button
type="button"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
className="flex h-8 w-8 items-center justify-center rounded-full bg-white/95 text-slate-700 shadow-lg backdrop-blur-sm transition-colors hover:bg-white hover:text-indigo-600 dark:bg-slate-900/95 dark:text-slate-300 dark:hover:text-indigo-400"
onClick={(event) => {
event.stopPropagation();
onFork(preset);
}}
title="另存为我的风格化"
>
<Copy className="h-3.5 w-3.5" />
</motion.button>
)}
</div>
</motion.div>
</div>
{/* 信息区域 */}
<div className="space-y-2.5 p-4">
<div>
<h3 className="line-clamp-1 text-sm font-semibold text-slate-900 dark:text-slate-100">
{preset.name}
</h3>
{preset.description && (
<p className="mt-1 line-clamp-1 text-xs text-slate-500 dark:text-slate-400">
{preset.description}
</p>
)}
</div>
<div className="flex items-center justify-between pt-1">
<div className="flex items-center gap-2">
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-slate-100 text-[10px] font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-400">
{ownerName.charAt(0).toUpperCase()}
</div>
<span className="max-w-[80px] truncate text-[11px] text-slate-500 dark:text-slate-400">
{ownerName}
</span>
</div>
<span className="text-[11px] text-slate-400 dark:text-slate-500">
{publishedAt}
</span>
</div>
{/* 槽位信息 */}
{preset.slotCount > 0 && (
<div className="flex items-center gap-3 border-t border-slate-100 pt-2.5 dark:border-slate-800">
<div className="flex items-center gap-1 text-[11px] text-slate-500 dark:text-slate-400">
<Sparkles className="h-3 w-3" />
<span>{preset.slotCount} 个槽位</span>
</div>
{preset.referenceAssetCount && preset.referenceAssetCount > 0 && (
<div className="flex items-center gap-1 text-[11px] text-slate-500 dark:text-slate-400">
<span>{preset.referenceAssetCount} 张参考图</span>
</div>
)}
</div>
)}
</div>
</button>
</motion.div>
);
}