1
0

Leaderboard.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. "use client";
  2. import { useEffect, useState } from "react";
  3. import { Agent } from "@/types/agent";
  4. import { Trophy, Star, User, Brain } from "lucide-react";
  5. import { cn } from "@/lib/utils";
  6. import { supabase } from "@/utils/supabase";
  7. import Link from "next/link";
  8. import { Header, LeaderboardHeader } from "@/components/header";
  9. import { motion, useScroll, useTransform } from "framer-motion";
  10. import { fetchAgentStats } from "@/lib/agents";
  11. type Score = {
  12. agentId: string;
  13. vrm: number;
  14. chatbot: number;
  15. tts: number;
  16. stt: number;
  17. vision: number;
  18. amicaLife: number;
  19. talentShowScore: number;
  20. };
  21. function AgentCard({ agent, rank, score }: { agent: Agent; rank: number; score: Score }) {
  22. const getRankStyle = (rank: number) => {
  23. switch (rank) {
  24. case 0:
  25. return {
  26. gradient: "from-amber-400 via-yellow-300 to-amber-400",
  27. border: "border-amber-300",
  28. shadow: "shadow-amber-200/50",
  29. icon: "text-amber-600",
  30. rank: "text-amber-700"
  31. };
  32. case 1:
  33. return {
  34. gradient: "from-slate-300 via-gray-200 to-slate-300",
  35. border: "border-slate-300",
  36. shadow: "shadow-slate-200/50",
  37. icon: "text-slate-600",
  38. rank: "text-slate-700"
  39. };
  40. case 2:
  41. return {
  42. gradient: "from-orange-300 via-amber-200 to-orange-300",
  43. border: "border-orange-300",
  44. shadow: "shadow-orange-200/50",
  45. icon: "text-orange-600",
  46. rank: "text-orange-700"
  47. };
  48. default:
  49. return {
  50. gradient: "from-slate-50 to-white",
  51. border: "border-slate-200",
  52. shadow: "shadow-slate-100/50",
  53. icon: "text-slate-500",
  54. rank: "text-slate-600"
  55. };
  56. }
  57. };
  58. const getCategoryTitle = (category: string) => {
  59. switch (category.toLowerCase()) {
  60. case 'security': return 'Security';
  61. case 'crypto': return 'Crypto';
  62. case 'personalAssistant': return 'Personal Assistant';
  63. case 'researcher': return 'Researcher';
  64. case 'programmer': return 'Programmer';
  65. default: return 'General';
  66. }
  67. };
  68. const getCategoryIcon = (category: string) => {
  69. switch (category.toLowerCase()) {
  70. case 'security': return '🛡️';
  71. case 'crypto': return '₿';
  72. case 'personalAssistant': return '👤';
  73. case 'researcher': return '🔬';
  74. case 'programmer': return '💻';
  75. default: return '🤖';
  76. }
  77. };
  78. const style = getRankStyle(rank);
  79. const isTopThree = rank < 3;
  80. return (
  81. <div role="link" className={`group relative overflow-hidden rounded-2xl transition-all duration-300 hover:scale-[1.02] hover:shadow-xl cursor-pointer ${isTopThree ? `bg-gradient-to-br ${style.gradient} ${style.border} ${style.shadow} shadow-lg border-2` : 'bg-white border border-slate-200 shadow-md hover:shadow-lg'}`}>
  82. {/* Rank Badge */}
  83. <div className={`absolute top-4 left-4 flex items-center justify-center w-12 h-12 rounded-full font-bold text-lg ${isTopThree ? 'bg-white/90 backdrop-blur-sm' : 'bg-slate-100'} ${style.rank} shadow-sm`}>
  84. #{rank + 1}
  85. </div>
  86. {/* Trophy for top 3 */}
  87. {isTopThree && (
  88. <div className={`absolute top-4 right-4 ${style.icon}`}>
  89. <Trophy className="w-6 h-6" />
  90. </div>
  91. )}
  92. <div className="p-6 pt-20">
  93. {/* Agent Profile */}
  94. <div className="flex items-center gap-4 mb-6">
  95. <div className="relative">
  96. <div className={`w-16 h-16 rounded-full overflow-hidden border-3 ${isTopThree ? 'border-white/60' : 'border-slate-200'} shadow-md`}>
  97. <img
  98. src={agent.avatar}
  99. alt={agent.name}
  100. className="w-full h-full object-cover"
  101. onError={(e) => {
  102. (e.target as HTMLImageElement).src = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect width="64" height="64" fill="%23e2e8f0"/><text x="32" y="36" text-anchor="middle" font-size="24" fill="%23475569">👤</text></svg>`;
  103. }}
  104. />
  105. </div>
  106. <div className="absolute -bottom-1 -right-1 text-lg">
  107. {getCategoryIcon(agent.category)}
  108. </div>
  109. </div>
  110. <div className="flex-1">
  111. <h3 className="text-xl font-bold text-slate-800 mb-1">{agent.name}</h3>
  112. <div className="flex items-center gap-2 text-sm text-slate-600 mb-1">
  113. <span className="px-2 py-1 bg-slate-100 rounded-full text-xs font-medium">
  114. {agent.tier?.name}
  115. </span>
  116. <span>•</span>
  117. <span>{getCategoryTitle(agent.category)}</span>
  118. </div>
  119. <p className="text-sm text-slate-500">{agent.description}</p>
  120. </div>
  121. </div>
  122. {/* Score Metrics */}
  123. <div className="space-y-4">
  124. {/* Overall Score */}
  125. <div className="flex items-center justify-between p-3 bg-white/60 backdrop-blur-sm rounded-xl border border-white/40">
  126. <div className="flex items-center gap-2">
  127. <Star className="w-5 h-5 text-violet-600" />
  128. <span className="font-semibold text-slate-700">Talent Score</span>
  129. </div>
  130. <span className="text-2xl font-bold text-violet-700">{score.talentShowScore.toFixed(1)}</span>
  131. </div>
  132. {/* Detailed Metrics Grid */}
  133. <div className="grid grid-cols-3 gap-3">
  134. {[
  135. { label: "VRM", value: score.vrm, icon: "🎭" },
  136. { label: "Vision", value: score.vision, icon: "👁️" },
  137. { label: "TTS", value: score.tts, icon: "🗣️" },
  138. { label: "STT", value: score.stt, icon: "👂" },
  139. { label: "Chatbot", value: score.chatbot, icon: "💬" },
  140. { label: "Amica Life", value: score.amicaLife, icon: "🌟" }
  141. ].map(({ label, value, icon }) => (
  142. <div key={label} className="text-center p-3 bg-white/40 backdrop-blur-sm rounded-lg border border-white/30">
  143. <div className="text-lg mb-1">{icon}</div>
  144. <div className="font-bold text-slate-700">{value.toFixed(0)}</div>
  145. <div className="text-xs text-slate-500 font-medium">{label}</div>
  146. </div>
  147. ))}
  148. </div>
  149. </div>
  150. </div>
  151. </div>
  152. );
  153. }
  154. export default function LeaderboardPage() {
  155. const { scrollY } = useScroll();
  156. const backgroundColor = useTransform(scrollY, [0, 300], ["rgb(26, 26, 46)", "rgb(255, 255, 255)"]);
  157. const [agents, setAgents] = useState<Agent[] | null>(null);
  158. const [scores, setScores] = useState<Score[]>([]);
  159. const [loading, setLoading] = useState<boolean>(true);
  160. const [checking, setChecking] = useState(true);
  161. const [error, setError] = useState<string | null>(null);
  162. // Fetchs agent
  163. useEffect(() => {
  164. let isMounted = true;
  165. const loadAgents = async () => {
  166. console.log("Load agents..")
  167. setLoading(true);
  168. setError(null);
  169. try {
  170. const res = await fetch("/api/agents");
  171. if (!res.ok) throw new Error("Failed to fetch agents. ");
  172. const data = await res.json();
  173. const agents = await fetchAgentStats(data);
  174. if (isMounted) {
  175. setAgents(Array.isArray(agents) ? agents : [agents]);
  176. }
  177. } catch (err: any) {
  178. if (isMounted) {
  179. setError(err.message || "Error loading agents.");
  180. }
  181. } finally {
  182. if (isMounted) {
  183. setLoading(false);
  184. }
  185. }
  186. };
  187. loadAgents();
  188. return () => {
  189. isMounted = false;
  190. };
  191. }, []);
  192. useEffect(() => {
  193. const fetchScores = async () => {
  194. console.log("Load scores..")
  195. const { data, error } = await supabase.from("agent-score").select("*");
  196. if (error) {
  197. console.error("Error fetching scores:", error.message);
  198. setChecking(false);
  199. return;
  200. }
  201. if (data) {
  202. const sorted = data.sort(
  203. (a, b) => b.talentShowScore - a.talentShowScore
  204. );
  205. setScores(sorted);
  206. setChecking(false);
  207. }
  208. };
  209. fetchScores();
  210. }, []);
  211. return (
  212. <motion.main className="min-h-screen" style={{ backgroundColor }}>
  213. {/* Hero Header */}
  214. <LeaderboardHeader />
  215. {error ? (
  216. <div className="sticky top-0 z-20 bg-white/80 backdrop-blur-sm border-b">
  217. <div className="p-4 text-red-500">Error loading agents: {error}</div>
  218. </div>
  219. ) : loading || checking ? (
  220. <div className="sticky top-0 z-20 bg-white/80 backdrop-blur-sm border-b">
  221. <div className="flex justify-center items-center p-8">
  222. <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
  223. </div>
  224. </div>
  225. ) : (
  226. <>
  227. {/* Leaderboard Content */}
  228. <div className="relative px-4 pb-16 sm:px-6 lg:px-8">
  229. <div className="max-w-4xl mx-auto">
  230. {/* Stats Bar */}
  231. <div className="flex justify-center mt-8 mb-12">
  232. <div className="flex items-center gap-8 px-6 py-3 bg-white rounded-full border border-gray-300 shadow">
  233. <div className="text-center">
  234. <div className="text-2xl font-bold text-black">{scores.length}</div>
  235. <div className="text-sm text-gray-700">Active Agents</div>
  236. </div>
  237. <div className="w-px h-8 bg-gray-300"></div>
  238. <div className="text-center">
  239. <div className="text-2xl font-bold text-black">
  240. {Math.max(...scores.map(s => s.talentShowScore)).toFixed(1)}
  241. </div>
  242. <div className="text-sm text-gray-700">Top Score</div>
  243. </div>
  244. <div className="w-px h-8 bg-gray-300"></div>
  245. <div className="text-center">
  246. <div className="text-2xl font-bold text-black">7</div>
  247. <div className="text-sm text-gray-700">Categories</div>
  248. </div>
  249. </div>
  250. </div>
  251. {/* Agent Cards */}
  252. <div className="grid gap-6">
  253. {scores.map((score, index) => {
  254. const agent = agents?.find(agent => agent.agentId === score.agentId);
  255. if (!agent) return null;
  256. return (
  257. <Link key={score.agentId} href={`/agent/${score.agentId}`} passHref>
  258. <AgentCard
  259. agent={agent}
  260. rank={index}
  261. score={score}
  262. />
  263. </Link>
  264. );
  265. })}
  266. </div>
  267. {/* Footer */}
  268. <div className="text-center mt-12 pt-8 border-t border-white/10">
  269. <p className="text-slate-400 text-sm">
  270. Rankings updated in real-time based on comprehensive AI capabilities assessment
  271. </p>
  272. </div>
  273. </div>
  274. </div>
  275. </>
  276. )}
  277. </motion.main >
  278. );
  279. }