0005e580a7
CI / build-and-push (push) Successful in 33s
Replace flat GPU buying with a realistic data center + rack pipeline: - 4 DC tiers (small/medium/large/mega) with construction time, dual capacity constraints (rack slots + power budget kW), and era/research gating - 10 predefined rack SKUs from consumer GPUs through custom ASICs, each with unique FLOPS, power draw, cost, and pipeline timings - 6-stage procurement pipeline (order → mfg → receive → install → test → production) with Kanban UI, talent-influenced speed bonuses - Test failures (5-25% base rate) reduced by cooling, ops talent, and QA research; auto-repair with cost and re-test cycle - Production failures at low per-tick rate, racks sent to repair pipeline - Cooling and redundancy upgrades per DC (reduce failure rates) - 4 new tech tree nodes (DC Engineering II/III/IV, Quality Assurance) - Save version bump (1→2) with migration that resets old saves - Updated economy system to account for rack repair costs - Redesigned Infrastructure page with pipeline Kanban, capacity bars, rack ordering, and DC upgrade panels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
269 lines
11 KiB
TypeScript
269 lines
11 KiB
TypeScript
import { useGameStore } from '@/store';
|
|
import { formatMoney, formatNumber, formatPercent } from '@ai-tycoon/shared';
|
|
import {
|
|
DollarSign, Server, Brain, Users, TrendingUp,
|
|
TrendingDown, Minus, Cpu, Zap, Shield,
|
|
} from 'lucide-react';
|
|
import { XAxis, YAxis, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts';
|
|
import { TutorialHint } from '@/components/game/TutorialHint';
|
|
|
|
export function DashboardPage() {
|
|
const money = useGameStore((s) => s.economy.money);
|
|
const revenuePerTick = useGameStore((s) => s.economy.revenuePerTick);
|
|
const expensesPerTick = useGameStore((s) => s.economy.expensesPerTick);
|
|
const totalFlops = useGameStore((s) => s.infrastructure.totalFlops);
|
|
const dataCenters = useGameStore((s) => s.infrastructure.dataCenters);
|
|
const trainedModels = useGameStore((s) => s.models.trainedModels);
|
|
const activeTraining = useGameStore((s) => s.models.activeTraining);
|
|
const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers);
|
|
const reputation = useGameStore((s) => s.reputation.score);
|
|
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
|
|
const financialHistory = useGameStore((s) => s.economy.financialHistory);
|
|
const era = useGameStore((s) => s.meta.currentEra);
|
|
|
|
const netIncome = revenuePerTick - expensesPerTick;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h2 className="text-2xl font-bold">Dashboard</h2>
|
|
|
|
{dataCenters.length === 0 && (
|
|
<TutorialHint id="welcome">
|
|
Welcome to AI Tycoon! Start by building a data center in the Infrastructure tab, then order racks to begin training your first AI model.
|
|
</TutorialHint>
|
|
)}
|
|
|
|
{dataCenters.length > 0 && trainedModels.length === 0 && !activeTraining && (
|
|
<TutorialHint id="train-first-model">
|
|
You have compute available! Head to the Models tab to allocate compute for training and start your first model.
|
|
</TutorialHint>
|
|
)}
|
|
|
|
{trainedModels.length > 0 && !trainedModels.some(m => m.isDeployed) && (
|
|
<TutorialHint id="deploy-model">
|
|
Your model is trained! Deploy it from the Models tab to start serving customers and earning revenue.
|
|
</TutorialHint>
|
|
)}
|
|
|
|
<div className="grid grid-cols-4 gap-4">
|
|
<StatCard
|
|
icon={DollarSign}
|
|
label="Cash"
|
|
value={formatMoney(money)}
|
|
subValue={`${netIncome >= 0 ? '+' : ''}${formatMoney(netIncome)}/s`}
|
|
trend={netIncome > 0 ? 'up' : netIncome < 0 ? 'down' : 'neutral'}
|
|
color="text-green-400"
|
|
/>
|
|
<StatCard
|
|
icon={Server}
|
|
label="Data Centers"
|
|
value={dataCenters.length.toString()}
|
|
subValue={`${formatNumber(totalFlops)} FLOPS`}
|
|
color="text-blue-400"
|
|
/>
|
|
<StatCard
|
|
icon={Brain}
|
|
label="Models"
|
|
value={trainedModels.length.toString()}
|
|
subValue={activeTraining ? `Training: ${Math.floor((activeTraining.progressTicks / activeTraining.totalTicks) * 100)}%` : 'Idle'}
|
|
color="text-purple-400"
|
|
/>
|
|
<StatCard
|
|
icon={Users}
|
|
label="Subscribers"
|
|
value={formatNumber(subscribers)}
|
|
subValue={`Satisfaction: ${formatPercent(useGameStore.getState().market.consumers.satisfaction)}`}
|
|
color="text-orange-400"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
<h3 className="text-sm font-medium text-surface-400 mb-4">Revenue Over Time</h3>
|
|
{financialHistory.length > 1 ? (
|
|
<ResponsiveContainer width="100%" height={200}>
|
|
<AreaChart data={financialHistory}>
|
|
<defs>
|
|
<linearGradient id="revenueGrad" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#22c55e" stopOpacity={0.3} />
|
|
<stop offset="100%" stopColor="#22c55e" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<XAxis dataKey="tick" hide />
|
|
<YAxis hide />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
|
labelStyle={{ color: '#94a3b8' }}
|
|
formatter={(value: number) => [formatMoney(value), 'Revenue']}
|
|
/>
|
|
<Area type="monotone" dataKey="revenue" stroke="#22c55e" fill="url(#revenueGrad)" />
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[200px] flex items-center justify-center text-surface-500 text-sm">
|
|
No data yet — start earning revenue
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
<h3 className="text-sm font-medium text-surface-400 mb-4">System Status</h3>
|
|
<div className="space-y-4">
|
|
<StatusRow
|
|
icon={Cpu}
|
|
label="Inference Utilization"
|
|
value={formatPercent(inferenceUtil)}
|
|
bar={inferenceUtil}
|
|
barColor={inferenceUtil > 0.9 ? 'bg-danger' : inferenceUtil > 0.7 ? 'bg-warning' : 'bg-success'}
|
|
/>
|
|
<StatusRow
|
|
icon={Shield}
|
|
label="Reputation"
|
|
value={`${reputation}/100`}
|
|
bar={reputation / 100}
|
|
barColor={reputation > 70 ? 'bg-success' : reputation > 40 ? 'bg-warning' : 'bg-danger'}
|
|
/>
|
|
<StatusRow
|
|
icon={Zap}
|
|
label="Compute"
|
|
value={`${formatNumber(totalFlops)} FLOPS`}
|
|
bar={Math.min(1, totalFlops / 100)}
|
|
barColor="bg-accent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
<h3 className="text-sm font-medium text-surface-400 mb-4">Subscribers Over Time</h3>
|
|
{(useGameStore.getState().market.subscriberHistory?.length ?? 0) > 1 ? (
|
|
<ResponsiveContainer width="100%" height={180}>
|
|
<AreaChart data={useGameStore.getState().market.subscriberHistory}>
|
|
<defs>
|
|
<linearGradient id="subsGrad" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#f97316" stopOpacity={0.3} />
|
|
<stop offset="100%" stopColor="#f97316" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<XAxis dataKey="tick" hide />
|
|
<YAxis hide />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
|
formatter={(value: number) => [formatNumber(value), 'Subscribers']}
|
|
/>
|
|
<Area type="monotone" dataKey="subscribers" stroke="#f97316" fill="url(#subsGrad)" />
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[180px] flex items-center justify-center text-surface-500 text-sm">
|
|
No subscriber data yet
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
<h3 className="text-sm font-medium text-surface-400 mb-4">Reputation Over Time</h3>
|
|
{(useGameStore.getState().reputation.reputationHistory?.length ?? 0) > 1 ? (
|
|
<ResponsiveContainer width="100%" height={180}>
|
|
<AreaChart data={useGameStore.getState().reputation.reputationHistory}>
|
|
<defs>
|
|
<linearGradient id="repGrad" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#a855f7" stopOpacity={0.3} />
|
|
<stop offset="100%" stopColor="#a855f7" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<XAxis dataKey="tick" hide />
|
|
<YAxis hide domain={[0, 100]} />
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
|
|
formatter={(value: number) => [`${value}/100`, 'Reputation']}
|
|
/>
|
|
<Area type="monotone" dataKey="score" stroke="#a855f7" fill="url(#repGrad)" />
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[180px] flex items-center justify-center text-surface-500 text-sm">
|
|
No reputation data yet
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{dataCenters.length === 0 && (
|
|
<div className="bg-surface-900 border border-accent/30 rounded-xl p-6 text-center">
|
|
<h3 className="text-lg font-semibold mb-2">Get Started</h3>
|
|
<p className="text-surface-400 text-sm mb-4">
|
|
Build your first data center to start training AI models.
|
|
</p>
|
|
<button
|
|
onClick={() => useGameStore.getState().setActivePage('infrastructure')}
|
|
className="bg-accent hover:bg-accent-dark text-white font-medium px-6 py-2 rounded-lg transition-colors"
|
|
>
|
|
Build Data Center
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatCard({
|
|
icon: Icon, label, value, subValue, trend, color,
|
|
}: {
|
|
icon: typeof DollarSign;
|
|
label: string;
|
|
value: string;
|
|
subValue?: string;
|
|
trend?: 'up' | 'down' | 'neutral';
|
|
color?: string;
|
|
}) {
|
|
return (
|
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Icon size={16} className={color ?? 'text-surface-400'} />
|
|
<span className="text-xs text-surface-400 uppercase tracking-wider">{label}</span>
|
|
</div>
|
|
<div className="text-2xl font-bold font-mono">{value}</div>
|
|
{subValue && (
|
|
<div className={`text-xs mt-1 flex items-center gap-1 ${
|
|
trend === 'up' ? 'text-success' : trend === 'down' ? 'text-danger' : 'text-surface-400'
|
|
}`}>
|
|
{trend === 'up' && <TrendingUp size={12} />}
|
|
{trend === 'down' && <TrendingDown size={12} />}
|
|
{trend === 'neutral' && <Minus size={12} />}
|
|
{subValue}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatusRow({
|
|
icon: Icon, label, value, bar, barColor,
|
|
}: {
|
|
icon: typeof Cpu;
|
|
label: string;
|
|
value: string;
|
|
bar: number;
|
|
barColor: string;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-1">
|
|
<div className="flex items-center gap-2">
|
|
<Icon size={14} className="text-surface-400" />
|
|
<span className="text-sm text-surface-300">{label}</span>
|
|
</div>
|
|
<span className="text-sm font-mono text-surface-200">{value}</span>
|
|
</div>
|
|
<div className="h-1.5 bg-surface-800 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full transition-all duration-500 ${barColor}`}
|
|
style={{ width: `${Math.min(100, bar * 100)}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|