4c1c0e9ff2
CI / build-and-push (push) Successful in 32s
Replace the single-stage training + flat capability score with a realistic AI development pipeline: pre-training with Chinchilla scaling laws, SFT with specializations, alignment with safety/capability tradeoffs (RLHF/DPO/Constitutional), model families with distillation/fine-tuning/quantization variants, named benchmark suite with compute-costing eval jobs, and segment-specific market quality. Phases 1-6 of the model rework plan: new types, engine rewrite, save migration, training events/risk system, concurrent training, variant creation, benchmark evaluation with leaderboard, and market integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
4.9 KiB
TypeScript
132 lines
4.9 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Trophy, Medal, Clock, TrendingUp } from 'lucide-react';
|
|
import { useGameStore } from '@/store';
|
|
import { formatMoney, formatNumber } from '@ai-tycoon/shared';
|
|
import { api, getAuthToken } from '@/lib/api';
|
|
|
|
interface LeaderboardEntry {
|
|
companyName: string;
|
|
score: number;
|
|
era: string;
|
|
tickCount: number;
|
|
}
|
|
|
|
const CATEGORIES = [
|
|
{ id: 'revenue', label: 'Highest Revenue', icon: TrendingUp },
|
|
{ id: 'fastest-agi', label: 'Fastest to AGI', icon: Clock },
|
|
{ id: 'capability', label: 'Best Model', icon: Trophy },
|
|
] as const;
|
|
|
|
export function LeaderboardPage() {
|
|
const [category, setCategory] = useState('revenue');
|
|
const [entries, setEntries] = useState<LeaderboardEntry[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [submitted, setSubmitted] = useState(false);
|
|
const companyName = useGameStore((s) => s.meta.companyName);
|
|
const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
|
|
const era = useGameStore((s) => s.meta.currentEra);
|
|
const tickCount = useGameStore((s) => s.meta.tickCount);
|
|
const bestModel = useGameStore((s) => s.models.bestDeployedModelScore);
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
api.leaderboard.get(category)
|
|
.then(data => setEntries(data.entries))
|
|
.catch(() => setEntries([]))
|
|
.finally(() => setLoading(false));
|
|
}, [category]);
|
|
|
|
const handleSubmit = async () => {
|
|
if (!getAuthToken()) return;
|
|
const scoreMap: Record<string, number> = {
|
|
revenue: Math.floor(totalRevenue),
|
|
'fastest-agi': era === 'agi' ? tickCount : 0,
|
|
capability: Math.floor(bestModel * 10),
|
|
};
|
|
const score = scoreMap[category];
|
|
if (score <= 0) return;
|
|
try {
|
|
await api.leaderboard.submit({ companyName, category, score, era, tickCount });
|
|
setSubmitted(true);
|
|
const data = await api.leaderboard.get(category);
|
|
setEntries(data.entries);
|
|
} catch { /* ignore */ }
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h2 className="text-2xl font-bold">Leaderboard</h2>
|
|
|
|
<div className="flex gap-2">
|
|
{CATEGORIES.map(cat => (
|
|
<button
|
|
key={cat.id}
|
|
onClick={() => { setCategory(cat.id); setSubmitted(false); }}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm border transition-colors ${
|
|
category === cat.id
|
|
? 'bg-accent/20 border-accent text-accent-light'
|
|
: 'bg-surface-900 border-surface-700 text-surface-300 hover:border-surface-500'
|
|
}`}
|
|
>
|
|
<cat.icon size={14} />
|
|
{cat.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{getAuthToken() && !submitted && (
|
|
<button
|
|
onClick={handleSubmit}
|
|
className="bg-accent hover:bg-accent-dark text-white px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
Submit My Score
|
|
</button>
|
|
)}
|
|
{submitted && <p className="text-sm text-success">Score submitted!</p>}
|
|
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl overflow-hidden">
|
|
{loading ? (
|
|
<div className="p-4 space-y-3">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div key={i} className="flex items-center gap-4 animate-pulse">
|
|
<div className="w-8 h-4 bg-surface-800 rounded" />
|
|
<div className="flex-1 h-4 bg-surface-800 rounded" />
|
|
<div className="w-16 h-4 bg-surface-800 rounded" />
|
|
<div className="w-12 h-4 bg-surface-800 rounded" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : entries.length === 0 ? (
|
|
<div className="p-8 text-center text-surface-500">
|
|
<Trophy size={48} className="mx-auto mb-4 opacity-50" />
|
|
<p>No entries yet. Be the first!</p>
|
|
</div>
|
|
) : (
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-surface-700 text-surface-400 text-xs uppercase">
|
|
<th className="p-3 text-left w-12">#</th>
|
|
<th className="p-3 text-left">Company</th>
|
|
<th className="p-3 text-right">Score</th>
|
|
<th className="p-3 text-right">Era</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{entries.map((entry, i) => (
|
|
<tr key={i} className="border-b border-surface-800 hover:bg-surface-800/50">
|
|
<td className="p-3">
|
|
{i < 3 ? <Medal size={16} className={i === 0 ? 'text-yellow-400' : i === 1 ? 'text-gray-400' : 'text-orange-400'} /> : i + 1}
|
|
</td>
|
|
<td className="p-3 font-medium">{entry.companyName}</td>
|
|
<td className="p-3 text-right font-mono">{formatNumber(entry.score)}</td>
|
|
<td className="p-3 text-right text-surface-400 capitalize">{entry.era}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|