Initial scaffold: AI Tycoon monorepo with core game loop
Turborepo monorepo with three packages: - packages/shared: TypeScript types for all 14 game systems + balance constants + formatting utils - packages/game-engine: Pure TS simulation engine with tick processor, economy, infrastructure, compute, research, market, and reputation systems - apps/web: React + Vite + Tailwind + Zustand frontend with sidebar dashboard layout, new game screen, dashboard with charts, infrastructure management, and model training pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import { useState } from 'react';
|
||||
import { Brain, Play, Rocket, Settings2 } from 'lucide-react';
|
||||
import { useGameStore } from '@/store';
|
||||
import { formatNumber, formatPercent, formatDuration } from '@ai-tycoon/shared';
|
||||
|
||||
export function ModelsPage() {
|
||||
const trainedModels = useGameStore((s) => s.models.trainedModels);
|
||||
const activeTraining = useGameStore((s) => s.models.activeTraining);
|
||||
const productLines = useGameStore((s) => s.models.productLines);
|
||||
const totalFlops = useGameStore((s) => s.compute.totalFlops);
|
||||
const trainingAlloc = useGameStore((s) => s.compute.trainingAllocation);
|
||||
const totalData = useGameStore((s) => s.data.totalTrainingTokens);
|
||||
const startTraining = useGameStore((s) => s.startTraining);
|
||||
const deployModel = useGameStore((s) => s.deployModel);
|
||||
const setTrainingAllocation = useGameStore((s) => s.setTrainingAllocation);
|
||||
|
||||
const [modelName, setModelName] = useState('');
|
||||
|
||||
const trainingFlops = totalFlops * trainingAlloc;
|
||||
const estimatedTicks = trainingFlops > 0 ? Math.max(30, Math.ceil(120 / (1 + trainingFlops * 0.1))) : Infinity;
|
||||
const estimatedCapability = Math.min(100, Math.log(1 + trainingFlops * 0.5) * 10 + Math.log(1 + totalData / 1e9) * 5);
|
||||
|
||||
const handleStartTraining = () => {
|
||||
if (activeTraining || trainingFlops === 0) return;
|
||||
const name = modelName.trim() || `Model v${trainedModels.length + 1}`;
|
||||
startTraining({
|
||||
modelName: name,
|
||||
generation: trainedModels.length + 1,
|
||||
allocatedCompute: trainingFlops,
|
||||
allocatedDataTokens: totalData,
|
||||
totalTicks: estimatedTicks,
|
||||
estimatedCapability,
|
||||
});
|
||||
setModelName('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold">Models</h2>
|
||||
|
||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||
<h3 className="font-semibold mb-3">Compute Allocation</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-surface-400 w-20">Training</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={trainingAlloc * 100}
|
||||
onChange={(e) => setTrainingAllocation(Number(e.target.value) / 100)}
|
||||
className="flex-1 accent-accent"
|
||||
/>
|
||||
<span className="text-sm text-surface-400 w-20 text-right">Inference</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-surface-500 mt-1">
|
||||
<span>{formatPercent(trainingAlloc)}</span>
|
||||
<span>{formatPercent(1 - trainingAlloc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-4">
|
||||
<h3 className="font-semibold">Train New Model</h3>
|
||||
{activeTraining ? (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium">{activeTraining.modelName}</span>
|
||||
<span className="text-sm text-surface-400">
|
||||
{formatPercent(activeTraining.progressTicks / activeTraining.totalTicks)} complete
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-surface-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-accent rounded-full transition-all duration-300"
|
||||
style={{ width: `${(activeTraining.progressTicks / activeTraining.totalTicks) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-surface-500 mt-1">
|
||||
ETA: {formatDuration(activeTraining.totalTicks - activeTraining.progressTicks)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs text-surface-400 mb-1">Model Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelName}
|
||||
onChange={(e) => setModelName(e.target.value)}
|
||||
placeholder={`Model v${trainedModels.length + 1}`}
|
||||
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
||||
<div className="bg-surface-800 rounded-lg p-3">
|
||||
<div className="text-xs text-surface-400">Training Compute</div>
|
||||
<div className="font-mono">{formatNumber(trainingFlops)} FLOPS</div>
|
||||
</div>
|
||||
<div className="bg-surface-800 rounded-lg p-3">
|
||||
<div className="text-xs text-surface-400">Training Data</div>
|
||||
<div className="font-mono">{formatNumber(totalData)} tokens</div>
|
||||
</div>
|
||||
<div className="bg-surface-800 rounded-lg p-3">
|
||||
<div className="text-xs text-surface-400">Est. Time</div>
|
||||
<div className="font-mono">{trainingFlops > 0 ? formatDuration(estimatedTicks) : 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-surface-400">
|
||||
Estimated capability score: <span className="text-accent-light font-mono">{estimatedCapability.toFixed(1)}/100</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleStartTraining}
|
||||
disabled={trainingFlops === 0}
|
||||
className="flex items-center gap-2 bg-accent hover:bg-accent-dark text-white px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Play size={16} />
|
||||
Start Training
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{trainedModels.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Trained Models</h3>
|
||||
{trainedModels.map(model => (
|
||||
<div key={model.id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium">{model.name}</h4>
|
||||
<div className="text-xs text-surface-400">
|
||||
Gen {model.generation} · Benchmark: {model.benchmarkScore.toFixed(1)}/100 · Safety: {model.safetyScore.toFixed(0)}/100
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{model.isDeployed ? (
|
||||
<span className="text-xs px-2 py-1 rounded-full bg-success/20 text-success">Deployed</span>
|
||||
) : (
|
||||
<>
|
||||
{productLines.filter(pl => pl.type === 'text-api' || pl.type === 'chat-product').map(pl => (
|
||||
<button
|
||||
key={pl.id}
|
||||
onClick={() => deployModel(model.id, pl.id)}
|
||||
className="flex items-center gap-1 bg-surface-800 hover:bg-surface-700 border border-surface-600 rounded px-3 py-1.5 text-xs"
|
||||
>
|
||||
<Rocket size={12} />
|
||||
Deploy to {pl.name}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Product Lines</h3>
|
||||
{productLines.map(pl => (
|
||||
<div key={pl.id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium">{pl.name}</h4>
|
||||
<div className="text-xs text-surface-400">
|
||||
{pl.modelId ? `Running: ${trainedModels.find(m => m.id === pl.modelId)?.name ?? 'Unknown'}` : 'No model deployed'}
|
||||
</div>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${pl.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}>
|
||||
{pl.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user