Files
AIHostingTycoon/apps/web/src/pages/ModelsPage.tsx
T
josh bbb69a315c Remove benchmark evaluation system, use training capabilities directly
Model quality for market segments and product lines now derives from deployed
model capabilities (coding, reasoning, agents, etc.) instead of requiring a
separate manual benchmark evaluation step. This eliminates an unbounded
benchmarkResults[] array that was scanned 5x per tick and removes ~480 lines
of dead-weight UI, types, and engine code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-26 19:28:59 -04:00

988 lines
48 KiB
TypeScript

import { useState } from 'react';
import { Play, Rocket, Globe, ChevronDown, ChevronUp, Beaker, Shield, Zap } from 'lucide-react';
import { TutorialHint } from '@/components/game/TutorialHint';
import { ConfirmModal } from '@/components/common/ConfirmModal';
import { useGameStore } from '@/store';
import {
formatNumber, formatPercent, formatDuration,
VRAM_REQUIREMENTS_BY_GENERATION, DEFAULT_DATA_MIX,
ALIGNMENT_METHODS,
QUANTIZATION_CONFIGS,
PARAMETER_OPTIONS,
SIZE_TIER_MAP,
SIZE_TIER_LABELS,
SFT_SPECIALIZATION_BONUSES,
PRETRAINING_BASE_TICKS,
} from '@ai-tycoon/shared';
import type {
ModelArchitecture, DataMixAllocation, SFTSpecialization, AlignmentMethod,
DataDomain, QuantizationLevel, BaseModel, ModelVariant,
SizeTier, ModelFamily,
} from '@ai-tycoon/shared';
const DATA_MIX_PRESETS: Record<string, { label: string; mix: DataMixAllocation }> = {
balanced: { label: 'Balanced', mix: DEFAULT_DATA_MIX },
'code-focused': { label: 'Code-Focused', mix: { web: 0.15, books: 0.05, code: 0.40, scientific: 0.15, conversation: 0.08, multilingual: 0.02, images: 0.03, video: 0.02, audio: 0.02, synthetic: 0.08 } },
creative: { label: 'Creative', mix: { web: 0.15, books: 0.30, code: 0.05, scientific: 0.05, conversation: 0.25, multilingual: 0.05, images: 0.05, video: 0.03, audio: 0.02, synthetic: 0.05 } },
research: { label: 'Research', mix: { web: 0.15, books: 0.10, code: 0.15, scientific: 0.35, conversation: 0.05, multilingual: 0.03, images: 0.02, video: 0.02, audio: 0.02, synthetic: 0.11 } },
};
const SFT_OPTIONS: { value: SFTSpecialization; label: string }[] = [
{ value: 'general', label: 'General' },
{ value: 'code', label: 'Code' },
{ value: 'math', label: 'Math' },
{ value: 'creative', label: 'Creative' },
{ value: 'multilingual', label: 'Multilingual' },
{ value: 'tool-use', label: 'Tool Use' },
];
const DOMAIN_LABELS: Record<DataDomain, string> = {
web: 'Web', books: 'Books', code: 'Code', scientific: 'Scientific',
conversation: 'Conversation', multilingual: 'Multilingual',
images: 'Images', video: 'Video', audio: 'Audio', synthetic: 'Synthetic',
};
const QUANT_LABELS: Record<QuantizationLevel, string> = {
fp16: 'FP16', int8: 'INT8', int4: 'INT4', int2: 'INT2',
};
export function ModelsPage() {
const baseModels = useGameStore((s) => s.models.baseModels);
const families = useGameStore((s) => s.models.families);
const pipelines = useGameStore((s) => s.models.activeTrainingPipelines);
const variantJobs = useGameStore((s) => s.models.variantJobs);
const productLines = useGameStore((s) => s.models.productLines);
const totalFlops = useGameStore((s) => s.compute.totalFlops);
const totalVramGB = useGameStore((s) => s.compute.totalVramGB);
const trainingAlloc = useGameStore((s) => s.compute.trainingAllocation);
const totalData = useGameStore((s) => s.data.totalTrainingTokens);
const currentEra = useGameStore((s) => s.meta.currentEra);
const startTrainingPipeline = useGameStore((s) => s.startTrainingPipeline);
const deployModel = useGameStore((s) => s.deployModel);
const deployVariant = useGameStore((s) => s.deployVariant);
const createQuantization = useGameStore((s) => s.createQuantization);
const setTrainingAllocation = useGameStore((s) => s.setTrainingAllocation);
const openSourceModel = useGameStore((s) => s.openSourceModel);
const openSourcedModels = useGameStore((s) => s.market.openSourcedModels);
const completedResearch = useGameStore((s) => s.research.completedResearch);
const modelsTab = useGameStore((s) => s.modelsTab);
const setModelsTab = useGameStore((s) => s.setModelsTab);
const [modelName, setModelName] = useState('');
const [expandedModel, setExpandedModel] = useState<string | null>(null);
const [expandedPipeline, setExpandedPipeline] = useState<string | null>(null);
const [parameterCount, setParameterCount] = useState(7);
const [contextWindow, setContextWindow] = useState(8);
const [archType, setArchType] = useState<'dense' | 'moe'>('dense');
const [dataMix, setDataMix] = useState<DataMixAllocation>({ ...DEFAULT_DATA_MIX });
const [dataMixPreset, setDataMixPreset] = useState('balanced');
// New model lifecycle state
const [familyMode, setFamilyMode] = useState<'new' | 'existing'>('new');
const [selectedFamilyId, setSelectedFamilyId] = useState<string | null>(null);
const [isPointRelease, setIsPointRelease] = useState(false);
const [sourceModelId, setSourceModelId] = useState<string | null>(null);
const [sftSpecs, setSftSpecs] = useState<SFTSpecialization[]>(['general']);
const [alignMethod, setAlignMethod] = useState<AlignmentMethod>('rlhf');
const [safetyWeight, setSafetyWeight] = useState(0.5);
const trainingFlops = totalFlops * trainingAlloc;
const estimatedTicks = trainingFlops > 0 ? Math.max(30, Math.ceil(PRETRAINING_BASE_TICKS / (1 + trainingFlops * 0.1))) : Infinity;
const estimatedCapability = Math.min(95, Math.sqrt(trainingFlops) * 5 + Math.log10(1 + totalData / 1e8) * 10);
const activePipelines = pipelines.filter(p => p.status === 'active' || p.status === 'stalled');
const activeVariantJobs = variantJobs.filter(j => j.status === 'active');
const undeployedCount = baseModels.filter(m => !m.isDeployed).length;
const hasActiveJobs = activePipelines.length > 0 || activeVariantJobs.length > 0;
const noModelDeployed = baseModels.length > 0 && !baseModels.some(m => m.isDeployed);
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'] as const;
const currentEraIdx = eraOrder.indexOf(currentEra);
const hasAlignmentResearch = completedResearch.some(r =>
r === 'alignment-research' || r === 'interpretability' || r === 'constitutional-ai',
);
// Computed size tier
const sizeTier: SizeTier = SIZE_TIER_MAP[parameterCount] ?? 'small';
// Model name preview
const familyNameForPreview = familyMode === 'new'
? (modelName.trim() || `Family ${families.length + 1}`)
: (families.find(f => f.id === selectedFamilyId)?.name ?? 'Family');
const nextVersion = (() => {
if (!isPointRelease || !sourceModelId) return 1.0;
const src = baseModels.find(m => m.id === sourceModelId);
return src ? Math.round((src.version + 0.1) * 10) / 10 : 1.0;
})();
const modelNamePreview = `${familyNameForPreview} ${SIZE_TIER_LABELS[sizeTier]} v${nextVersion.toFixed(1)}`;
const handleStartTraining = () => {
if (trainingFlops === 0) return;
const architecture: ModelArchitecture = {
type: archType,
totalParameters: parameterCount,
activeParameters: archType === 'moe' ? Math.ceil(parameterCount * 0.25) : parameterCount,
contextWindow,
vocabularySize: 32000,
...(archType === 'moe' ? { expertCount: 8, expertTopK: 2 } : {}),
};
startTrainingPipeline({
...(familyMode === 'new'
? { familyName: modelName.trim() || `Family ${families.length + 1}` }
: { familyId: selectedFamilyId! }),
architecture,
dataMix,
allocatedComputeFraction: 1.0,
targetTokens: totalData,
totalTicks: estimatedTicks,
sftSpecializations: sftSpecs,
alignmentMethod: alignMethod,
alignmentSafetyWeight: safetyWeight,
isPointRelease,
sourceModelId: sourceModelId ?? undefined,
});
setModelName('');
setIsPointRelease(false);
setSourceModelId(null);
};
const handlePresetChange = (presetKey: string) => {
setDataMixPreset(presetKey);
const preset = DATA_MIX_PRESETS[presetKey];
if (preset) setDataMix({ ...preset.mix });
};
const handleMixSlider = (domain: DataDomain, value: number) => {
const newMix = { ...dataMix, [domain]: value / 100 };
const total = Object.values(newMix).reduce((s, v) => s + v, 0);
if (total > 0) {
for (const key of Object.keys(newMix) as DataDomain[]) {
newMix[key] = newMix[key] / total;
}
}
setDataMix(newMix);
setDataMixPreset('custom');
};
return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Models</h2>
<TutorialHint id="models-intro">
Split compute between training (building new models) and inference (serving customers). Deploy trained models to start earning revenue.
</TutorialHint>
<div className="flex gap-1 border-b border-surface-700 pb-px">
{([
{ id: 'overview' as const, label: 'Overview' },
{ id: 'train' as const, label: 'Train New' },
{ id: 'models' as const, label: `Families${families.length > 0 ? ` (${families.length})` : ''}` },
{ id: 'products' as const, label: 'Products' },
]).map(tab => (
<button
key={tab.id}
onClick={() => setModelsTab(tab.id)}
className={`px-4 py-2 text-sm rounded-t-lg transition-colors flex items-center gap-1 ${
modelsTab === tab.id
? 'bg-surface-800 text-surface-100 border-b-2 border-accent'
: 'text-surface-400 hover:text-surface-200 hover:bg-surface-800/50'
}`}
>
{tab.label}
{tab.id === 'overview' && hasActiveJobs && (
<span className="w-2 h-2 rounded-full bg-accent animate-pulse" />
)}
{tab.id === 'models' && undeployedCount > 0 && (
<span className="px-1.5 py-0.5 text-[9px] rounded-full bg-warning/20 text-warning">{undeployedCount}</span>
)}
{tab.id === 'products' && noModelDeployed && (
<span className="w-2 h-2 rounded-full bg-warning" />
)}
</button>
))}
</div>
{/* Compute Allocation — always visible */}
<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>
{/* Active Training Pipelines */}
{modelsTab === 'overview' && activePipelines.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold">Active Training</h3>
{activePipelines.map(pipeline => {
const stage = pipeline.currentStage === 'pretraining' ? pipeline.stages.pretraining
: pipeline.currentStage === 'sft' ? pipeline.stages.sft
: pipeline.stages.alignment;
if (!stage) return null;
const progress = stage.progressTicks / stage.totalTicks;
const generation = families.find(f => f.id === pipeline.familyId)?.generation ?? 1;
const reqVram = VRAM_REQUIREMENTS_BY_GENERATION[generation] ?? 0;
const isStalled = pipeline.status === 'stalled';
const isExpanded = expandedPipeline === pipeline.id;
const stageLabel = pipeline.currentStage === 'pretraining' ? 'Pre-training'
: pipeline.currentStage === 'sft' ? 'SFT' : 'Alignment';
const recentEvents = pipeline.events.slice(-3).reverse();
return (
<div key={pipeline.id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<button onClick={() => setExpandedPipeline(isExpanded ? null : pipeline.id)} className="text-surface-400 hover:text-surface-200">
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
<div>
<span className="text-sm font-medium">{pipeline.modelName}</span>
<span className="text-xs text-surface-500 ml-2">
{pipeline.architecture.totalParameters}B {pipeline.architecture.type.toUpperCase()} · {pipeline.architecture.contextWindow}K ctx
</span>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs px-2 py-0.5 rounded bg-surface-700 text-surface-300">{stageLabel}</span>
<span className="text-sm text-surface-400">
{isStalled ? <span className="text-error">Stalled</span> : `${formatPercent(progress)}`}
</span>
</div>
</div>
<div className="flex gap-1 mb-2">
<StageBar label="Pre" active={pipeline.currentStage === 'pretraining'} complete={pipeline.stages.pretraining.isComplete} progress={pipeline.stages.pretraining.progressTicks / pipeline.stages.pretraining.totalTicks} />
<StageBar label="SFT" active={pipeline.currentStage === 'sft'} complete={pipeline.stages.sft.isComplete} progress={pipeline.stages.sft.progressTicks / pipeline.stages.sft.totalTicks} />
<StageBar label="Align" active={pipeline.currentStage === 'alignment'} complete={pipeline.stages.alignment.isComplete} progress={pipeline.stages.alignment.progressTicks / pipeline.stages.alignment.totalTicks} />
</div>
<div className="h-2 bg-surface-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-300 ${isStalled ? 'bg-error' : 'bg-accent'}`}
style={{ width: `${progress * 100}%` }}
/>
</div>
<div className="text-xs text-surface-500 mt-1">
{isStalled
? `Requires ${formatNumber(reqVram)} GB VRAM (have ${formatNumber(totalVramGB)} GB)`
: `ETA: ${formatDuration(stage.totalTicks - stage.progressTicks)}`}
</div>
{isExpanded && (
<div className="mt-3 pt-3 border-t border-surface-700 space-y-2">
{pipeline.currentStage === 'pretraining' && (
<div className="text-xs text-surface-400">
Loss: <span className="font-mono text-surface-300">{pipeline.stages.pretraining.lossValue.toFixed(3)}</span>
{' · '}Chinchilla ratio: <span className="font-mono text-surface-300">{pipeline.stages.pretraining.chinchillaRatio.toFixed(1)}</span>
</div>
)}
{recentEvents.length > 0 && (
<div className="space-y-1">
<span className="text-xs text-surface-500 font-medium">Recent Events</span>
{recentEvents.map(event => (
<div key={event.id} className={`text-xs px-2 py-1 rounded ${
event.type === 'breakthrough' || event.type === 'emergent_capability' ? 'bg-success/10 text-success' :
event.type === 'loss_spike' || event.type === 'instability' ? 'bg-error/10 text-error' :
'bg-warning/10 text-warning'
}`}>
{event.description}
</div>
))}
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
{/* Active Variant Jobs */}
{modelsTab === 'overview' && activeVariantJobs.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold">Variant Jobs</h3>
{activeVariantJobs.map(job => {
const base = baseModels.find(m => m.id === job.baseModelId);
const progress = job.progressTicks / job.totalTicks;
return (
<div key={job.id} className="bg-surface-900 border border-surface-700 rounded-xl p-3">
<div className="flex items-center justify-between mb-1">
<span className="text-sm">{('variantName' in job.config ? (job.config as { variantName: string }).variantName : base?.name) ?? 'Variant'}</span>
<span className="text-xs px-2 py-0.5 rounded bg-surface-700 text-surface-300 capitalize">{job.jobType}</span>
</div>
<div className="h-1.5 bg-surface-800 rounded-full overflow-hidden">
<div className="h-full bg-accent rounded-full transition-all" style={{ width: `${progress * 100}%` }} />
</div>
<div className="text-xs text-surface-500 mt-1">
{formatPercent(progress)} · ETA: {formatDuration(job.totalTicks - job.progressTicks)}
</div>
</div>
);
})}
</div>
)}
{/* Train New Model */}
{modelsTab === 'train' && <div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-4">
<h3 className="font-semibold">Train New Model</h3>
{isPointRelease && sourceModelId && (
<div className="bg-accent/10 border border-accent/30 rounded-lg px-3 py-2 flex items-center justify-between">
<div className="text-sm text-accent-light">
Point Release iterating on <span className="font-mono">{baseModels.find(m => m.id === sourceModelId)?.name ?? 'model'}</span>
<span className="text-xs text-surface-400 ml-2">(40% training time)</span>
</div>
<button onClick={() => { setIsPointRelease(false); setSourceModelId(null); }} className="text-xs text-surface-400 hover:text-surface-200">Cancel</button>
</div>
)}
<div className="space-y-3">
{/* Family selector */}
<div>
<label className="block text-xs text-surface-400 mb-1">Model Family</label>
<div className="flex gap-2 mb-2">
<button
onClick={() => { setFamilyMode('new'); setIsPointRelease(false); setSourceModelId(null); }}
className={`flex-1 px-3 py-2 rounded text-sm border transition-colors ${familyMode === 'new' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-300'}`}
>
New Family
</button>
<button
onClick={() => setFamilyMode('existing')}
disabled={families.length === 0}
className={`flex-1 px-3 py-2 rounded text-sm border transition-colors ${familyMode === 'existing' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-300'} disabled:opacity-50 disabled:cursor-not-allowed`}
>
Add to Family
</button>
</div>
{familyMode === 'new' ? (
<input
type="text" value={modelName}
onChange={(e) => setModelName(e.target.value)}
placeholder={`Family ${families.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"
/>
) : (
<select
value={selectedFamilyId ?? ''}
onChange={(e) => setSelectedFamilyId(e.target.value || null)}
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"
>
<option value="">Select a family...</option>
{families.map(f => (
<option key={f.id} value={f.id}>{f.name} (Gen {f.generation})</option>
))}
</select>
)}
</div>
{/* Architecture & Parameters */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-surface-400 mb-1">Architecture</label>
<div className="flex gap-2">
<button
onClick={() => setArchType('dense')}
className={`flex-1 px-3 py-2 rounded text-sm border transition-colors ${archType === 'dense' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-300'}`}
>
Dense
</button>
<button
onClick={() => setArchType('moe')}
className={`flex-1 px-3 py-2 rounded text-sm border transition-colors ${archType === 'moe' ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-300'}`}
>
MoE
</button>
</div>
</div>
<div>
<label className="block text-xs text-surface-400 mb-1">Context Window</label>
<select
value={contextWindow}
onChange={(e) => setContextWindow(Number(e.target.value))}
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"
>
{[4, 8, 32, 128, 256, 1024].map(c => (
<option key={c} value={c}>{c >= 1024 ? `${c / 1024}M` : `${c}K`}</option>
))}
</select>
</div>
</div>
{/* Parameters with size tier indicator */}
<div>
<label className="block text-xs text-surface-400 mb-1">Parameters (Billions)</label>
<div className="flex items-center gap-3">
<select
value={parameterCount}
onChange={(e) => setParameterCount(Number(e.target.value))}
className="flex-1 bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
>
{PARAMETER_OPTIONS.map(p => (
<option key={p} value={p}>{p}B</option>
))}
</select>
<span className="text-xs px-2 py-1 rounded bg-accent/10 text-accent-light border border-accent/30">
{SIZE_TIER_LABELS[sizeTier]}
</span>
</div>
</div>
{/* Data Mix */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-surface-400">Training Data Mix</label>
<div className="flex gap-1">
{Object.entries(DATA_MIX_PRESETS).map(([key, preset]) => (
<button
key={key}
onClick={() => handlePresetChange(key)}
className={`px-2 py-0.5 rounded text-[10px] border transition-colors ${
dataMixPreset === key ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{preset.label}
</button>
))}
</div>
</div>
<p className="text-[10px] text-surface-500 mb-1">Total must equal 100% other values adjust proportionally.</p>
<div className="grid grid-cols-2 gap-x-4 gap-y-1">
{(Object.keys(DOMAIN_LABELS) as DataDomain[]).map(domain => (
<div key={domain} className="flex items-center gap-2">
<span className="text-[10px] text-surface-400 w-16">{DOMAIN_LABELS[domain]}</span>
<input
type="range" min={0} max={100}
value={Math.round(dataMix[domain] * 100)}
onChange={(e) => handleMixSlider(domain, Number(e.target.value))}
className="flex-1 accent-accent h-1"
/>
<span className="text-[10px] font-mono text-surface-500 w-8 text-right">{Math.round(dataMix[domain] * 100)}%</span>
</div>
))}
</div>
</div>
{/* SFT Configuration */}
<div>
<div className="flex items-center gap-1 mb-2">
<Beaker size={12} className="text-surface-400" />
<label className="text-xs text-surface-400">SFT Specializations</label>
</div>
<div className="flex flex-wrap gap-1">
{SFT_OPTIONS.map(opt => (
<button
key={opt.value}
onClick={() => setSftSpecs(prev =>
prev.includes(opt.value)
? prev.filter(s => s !== opt.value)
: [...prev, opt.value]
)}
className={`px-2 py-1 rounded text-xs border transition-colors ${
sftSpecs.includes(opt.value)
? 'bg-accent/20 border-accent text-accent-light'
: 'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{opt.label}
</button>
))}
</div>
{sftSpecs.length > 0 && (
<div className="mt-2 text-[10px] text-surface-500">
<span className="font-medium text-surface-400">Bonus preview: </span>
{sftSpecs.map(spec => {
const bonuses = SFT_SPECIALIZATION_BONUSES[spec];
if (!bonuses) return null;
const positives = Object.entries(bonuses).filter(([, v]) => v > 0).map(([k, v]) => `${k} +${v}`);
const negatives = Object.entries(bonuses).filter(([, v]) => v < 0).map(([k, v]) => `${k} ${v}`);
return (
<span key={spec} className="inline-block mr-2">
<span className="text-accent-light">{spec}</span>
{positives.length > 0 && <span className="text-success ml-1">{positives.join(', ')}</span>}
{negatives.length > 0 && <span className="text-error ml-1">{negatives.join(', ')}</span>}
</span>
);
})}
</div>
)}
</div>
{/* Alignment Configuration */}
<div>
<div className="flex items-center gap-1 mb-2">
<Shield size={12} className="text-surface-400" />
<label className="text-xs text-surface-400">Alignment</label>
</div>
{hasAlignmentResearch ? (
<div className="space-y-2">
<div className="flex gap-1">
{(Object.keys(ALIGNMENT_METHODS) as AlignmentMethod[]).map(method => {
const isAvailable = completedResearch.includes(ALIGNMENT_METHODS[method].requiredResearch);
return (
<button
key={method}
disabled={!isAvailable}
onClick={() => setAlignMethod(method)}
className={`px-2 py-1 rounded text-xs border transition-colors ${
alignMethod === method ? 'bg-accent/20 border-accent text-accent-light' :
!isAvailable ? 'bg-surface-800 border-surface-700 text-surface-600 cursor-not-allowed' :
'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{method.toUpperCase()}
</button>
);
})}
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] text-surface-400">Safety</span>
<input type="range" min={0} max={100} value={safetyWeight * 100}
onChange={(e) => setSafetyWeight(Number(e.target.value) / 100)}
className="flex-1 accent-accent h-1" />
<span className="text-[10px] text-surface-400">Helpful</span>
<span className="text-[10px] font-mono text-surface-500 w-8 text-right">{Math.round(safetyWeight * 100)}%</span>
</div>
</div>
) : (
<div className="text-xs text-surface-500 flex items-center gap-1">
<Shield size={10} /> Requires alignment research defaults to RLHF
</div>
)}
</div>
{/* Stats */}
<div className="grid grid-cols-4 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">Available VRAM</div>
<div className="font-mono">{formatNumber(totalVramGB)} GB</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: <span className="text-accent-light font-mono">{estimatedCapability.toFixed(1)}/100</span>
{archType === 'moe' && <span className="ml-2 text-xs text-accent">(+15% MoE bonus)</span>}
</div>
{/* Model name preview */}
<div className="bg-surface-800/50 rounded-lg px-3 py-2 flex items-center gap-2">
<span className="text-[10px] text-surface-500">Model name:</span>
<span className="text-sm font-mono text-surface-300">{modelNamePreview}</span>
</div>
{/* Start button */}
<div>
<button
onClick={handleStartTraining}
disabled={trainingFlops === 0 || (familyMode === 'existing' && !selectedFamilyId)}
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} />
{isPointRelease ? 'Start Point Release' : 'Start Training'}
</button>
{trainingFlops === 0 && totalFlops === 0 && (
<p className="text-xs text-warning mt-1">Build a data center and order racks first</p>
)}
{trainingFlops === 0 && totalFlops > 0 && (
<p className="text-xs text-warning mt-1">Allocate compute to training above</p>
)}
</div>
</div>
</div>}
{/* Model Families & Trained Models */}
{modelsTab === 'models' && families.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold">Model Families</h3>
{families.map(family => {
const familyModels = baseModels.filter(m => m.familyId === family.id);
const variants = family.variants;
const isExpanded = expandedModel === family.id;
return (
<div key={family.id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
{/* Family header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<button onClick={() => setExpandedModel(isExpanded ? null : family.id)} className="text-surface-400 hover:text-surface-200">
{isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
<h4 className="font-medium">{family.name} <span className="text-xs text-surface-500">Gen {family.generation}</span></h4>
</div>
<div className="flex items-center gap-2">
<button onClick={() => { setModelsTab('train'); setFamilyMode('existing'); setSelectedFamilyId(family.id); }} className="text-xs text-accent hover:text-accent-light">
+ Train New Size
</button>
</div>
</div>
{/* Model rows */}
{familyModels.map(model => (
<div key={model.id} className="flex items-center justify-between py-2 border-t border-surface-800 text-sm">
<div className="flex items-center gap-3">
<span className="font-medium">{model.name}</span>
<span className="text-xs text-surface-500">{model.architecture.totalParameters}B</span>
<span className="text-xs text-surface-500">Cap: {model.rawCapability.toFixed(1)}</span>
</div>
<div className="flex items-center gap-2">
{model.isDeployed ? (
<span className="text-xs px-2 py-0.5 rounded-full bg-success/20 text-success">Deployed</span>
) : (
<button onClick={() => deployModel(model.id)} className="text-xs bg-accent hover:bg-accent-dark text-white rounded px-2 py-1">Deploy</button>
)}
<button onClick={() => {
setModelsTab('train');
setFamilyMode('existing');
setSelectedFamilyId(family.id);
setIsPointRelease(true);
setSourceModelId(model.id);
setParameterCount(model.architecture.totalParameters);
setArchType(model.architecture.type);
setContextWindow(model.architecture.contextWindow);
setDataMix(model.dataMix);
setSftSpecs(model.sftSpecializations);
setAlignMethod(model.alignmentMethod ?? 'rlhf');
}} className="text-xs text-surface-400 hover:text-accent">Iterate</button>
</div>
</div>
))}
{familyModels.length === 0 && (
<p className="text-xs text-surface-500 py-2">Training in progress...</p>
)}
{/* Expanded: details, quantize, eval for each model */}
{isExpanded && familyModels.length > 0 && (
<div className="mt-4 pt-4 border-t border-surface-700 space-y-4">
{familyModels.map(model => (
<div key={model.id} className="space-y-3">
<h5 className="text-sm font-medium text-surface-300">{model.name}</h5>
<ModelDetails model={model} />
<QuantizationCreator model={model} completedResearch={completedResearch} onQuantize={createQuantization} />
</div>
))}
{variants.length > 0 && (
<div className="space-y-2">
<span className="text-xs font-medium text-surface-300">Quantized Variants</span>
{variants.map(variant => (
<VariantCard
key={variant.id}
variant={variant}
familyId={family.id}
onDeploy={() => deployVariant(family.id, variant.id)}
/>
))}
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
{/* Product Lines */}
{modelsTab === 'products' && <div className="space-y-3">
<h3 className="font-semibold">Product Lines</h3>
{noModelDeployed && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-6 text-center space-y-3">
<p className="text-surface-400 text-sm">No model deployed yet. Deploy a model to start earning revenue.</p>
<button onClick={() => setModelsTab('models')} className="inline-flex items-center gap-2 bg-accent hover:bg-accent/80 text-white px-4 py-2 rounded-lg text-sm">
<Rocket size={16} /> Go to Families
</button>
</div>
)}
{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: ${baseModels.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>}
{modelsTab === 'models' && families.length === 0 && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center space-y-3">
<p className="text-surface-400 text-sm">No model families yet. Train your first model to get started.</p>
<button onClick={() => setModelsTab('train')} className="inline-flex items-center gap-2 bg-accent hover:bg-accent/80 text-white px-4 py-2 rounded-lg text-sm">
<Play size={16} /> Train Your First Model
</button>
</div>
)}
{modelsTab === 'overview' && !hasActiveJobs && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center space-y-3">
<p className="text-surface-400 text-sm">No active training or evaluation jobs.</p>
<div className="flex justify-center gap-3">
<button onClick={() => setModelsTab('train')} className="inline-flex items-center gap-2 bg-accent hover:bg-accent/80 text-white px-4 py-2 rounded-lg text-sm">
<Play size={16} /> {families.length === 0 ? 'Train Your First Model' : 'Train New Model'}
</button>
{families.length > 0 && (
<button onClick={() => setModelsTab('models')} className="inline-flex items-center gap-2 bg-surface-700 hover:bg-surface-600 text-surface-200 px-4 py-2 rounded-lg text-sm">
View Families ({families.length})
</button>
)}
</div>
</div>
)}
</div>
);
}
function ModelActions({ model, isOpenSourced, onDeploy, onOpenSource }: {
model: BaseModel;
isOpenSourced: boolean;
onDeploy: () => void;
onOpenSource: () => void;
}) {
const [confirmAction, setConfirmAction] = useState<'deploy' | 'opensource' | null>(null);
return (
<>
{!isOpenSourced && model.isDeployed && (
<button onClick={() => setConfirmAction('opensource')}
className="flex items-center gap-1 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border border-blue-600/30 rounded px-3 py-1.5 text-xs">
<Globe size={12} /> Open Source
</button>
)}
{model.isDeployed ? (
<span className="text-xs px-2 py-1 rounded-full bg-success/20 text-success">Deployed</span>
) : (
<button onClick={() => setConfirmAction('deploy')}
className="flex items-center gap-1 bg-accent hover:bg-accent-dark text-white rounded px-3 py-1.5 text-xs">
<Rocket size={12} /> Deploy
</button>
)}
{confirmAction === 'deploy' && (
<ConfirmModal
title="Deploy Model"
message={`Deploy "${model.name}" to production? All product lines will use this model for inference.`}
confirmLabel="Deploy"
onConfirm={() => { onDeploy(); setConfirmAction(null); }}
onCancel={() => setConfirmAction(null)}
/>
)}
{confirmAction === 'opensource' && (
<ConfirmModal
title="Open Source Model"
message={`Open source "${model.name}"? This will make the model publicly available. Your competitors will benefit from it. This cannot be undone.`}
confirmLabel="Open Source"
danger
onConfirm={() => { onOpenSource(); setConfirmAction(null); }}
onCancel={() => setConfirmAction(null)}
/>
)}
</>
);
}
function ModelDetails({ model }: { model: BaseModel }) {
return (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3 text-xs">
<div className="bg-surface-800 rounded-lg p-2">
<span className="text-surface-400">Architecture</span>
<div className="font-mono mt-0.5">{model.architecture.totalParameters}B {model.architecture.type}</div>
</div>
<div className="bg-surface-800 rounded-lg p-2">
<span className="text-surface-400">Context</span>
<div className="font-mono mt-0.5">{model.architecture.contextWindow >= 1024 ? `${model.architecture.contextWindow / 1024}M` : `${model.architecture.contextWindow}K`}</div>
</div>
<div className="bg-surface-800 rounded-lg p-2">
<span className="text-surface-400">Stages</span>
<div className="font-mono mt-0.5">{model.trainingStagesCompleted.join(' + ')}</div>
</div>
</div>
<div className="grid grid-cols-3 gap-3 text-xs">
{(['reasoning', 'coding', 'creative', 'math', 'knowledge', 'multimodal', 'agents', 'speed', 'contextUtilization'] as const).map(cap => (
<div key={cap} className="bg-surface-800 rounded-lg p-2">
<span className="text-surface-400 capitalize">{cap === 'contextUtilization' ? 'Context Util.' : cap}</span>
<div className="font-mono mt-0.5">{model.capabilities[cap].toFixed(1)}</div>
</div>
))}
</div>
<div className="grid grid-cols-3 gap-3 text-xs">
<div className="bg-surface-800 rounded-lg p-2">
<span className="text-surface-400">Safety</span>
<div className="font-mono mt-0.5">{model.safetyProfile.overallSafety.toFixed(1)}</div>
</div>
<div className="bg-surface-800 rounded-lg p-2">
<span className="text-surface-400">Harm Avoidance</span>
<div className="font-mono mt-0.5">{model.safetyProfile.harmAvoidance.toFixed(1)}</div>
</div>
<div className="bg-surface-800 rounded-lg p-2">
<span className="text-surface-400">Refusal Rate</span>
<div className="font-mono mt-0.5">{formatPercent(model.safetyProfile.refusalRate)}</div>
</div>
</div>
</div>
);
}
function QuantizationCreator({ model, completedResearch, onQuantize }: {
model: BaseModel;
completedResearch: string[];
onQuantize: (baseModelId: string, level: QuantizationLevel, name: string) => void;
}) {
const [showCreator, setShowCreator] = useState(false);
const [quantLevel, setQuantLevel] = useState<QuantizationLevel>('int8');
const hasQuantization = completedResearch.includes('quantization') || completedResearch.includes('model-compression');
if (!hasQuantization) return null;
if (!showCreator) {
return (
<button
onClick={() => setShowCreator(true)}
className="flex items-center gap-1 text-xs text-accent hover:text-accent-light"
>
<Zap size={12} /> Create Quantized Variant
</button>
);
}
return (
<div className="bg-surface-800/50 rounded-lg p-3 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-surface-300">Quantize {model.name}</span>
<button onClick={() => setShowCreator(false)} className="text-xs text-surface-500 hover:text-surface-300">Close</button>
</div>
<div className="space-y-2">
<div>
<label className="text-[10px] text-surface-400">Quantization Level</label>
<div className="flex gap-1 mt-0.5">
{(Object.keys(QUANTIZATION_CONFIGS) as QuantizationLevel[]).map(level => {
const cfg = QUANTIZATION_CONFIGS[level];
return (
<button key={level} onClick={() => setQuantLevel(level)}
className={`flex-1 px-2 py-1 rounded text-[10px] border ${quantLevel === level ? 'bg-accent/20 border-accent text-accent-light' : 'bg-surface-800 border-surface-600 text-surface-400'}`}>
<div>{QUANT_LABELS[level]}</div>
<div className="text-[9px] text-surface-500">{(cfg.qualityRetention * 100).toFixed(0)}% · {cfg.speedMultiplier}x</div>
</button>
);
})}
</div>
</div>
<button onClick={() => { onQuantize(model.id, quantLevel, `${model.name}-Turbo`); setShowCreator(false); }}
className="bg-accent hover:bg-accent-dark text-white rounded px-3 py-1 text-xs">
Start Quantization
</button>
</div>
</div>
);
}
function VariantCard({ variant, familyId, onDeploy }: {
variant: ModelVariant;
familyId: string;
onDeploy: () => void;
}) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="bg-surface-800/50 rounded-lg p-3 ml-4 border-l-2 border-surface-600">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<button onClick={() => setIsExpanded(!isExpanded)} className="text-surface-400 hover:text-surface-200">
{isExpanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
</button>
<div>
<span className="text-sm font-medium">{variant.name}</span>
<span className="text-[10px] ml-2 text-green-400">Quantized</span>
{variant.quantization && <span className="text-[10px] text-surface-500 ml-1">{variant.quantization.toUpperCase()}</span>}
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] text-surface-500">
{variant.costMultiplier < 1 ? `${(variant.costMultiplier * 100).toFixed(0)}% cost` : ''}
{variant.speedMultiplier > 1 ? ` ${variant.speedMultiplier.toFixed(1)}x speed` : ''}
</span>
{variant.isDeployed ? (
<span className="text-xs px-2 py-0.5 rounded-full bg-success/20 text-success">Deployed</span>
) : (
<button onClick={onDeploy}
className="flex items-center gap-1 bg-accent hover:bg-accent-dark text-white rounded px-2 py-1 text-[10px]">
<Rocket size={10} /> Deploy
</button>
)}
</div>
</div>
{isExpanded && (
<div className="mt-3 pt-2 border-t border-surface-700 space-y-2">
<div className="grid grid-cols-3 gap-2 text-xs">
{(['reasoning', 'coding', 'creative', 'math', 'knowledge', 'speed'] as const).map(cap => (
<div key={cap} className="bg-surface-800 rounded p-1.5">
<span className="text-surface-400 capitalize text-[10px]">{cap}</span>
<div className="font-mono text-[11px]">{variant.capabilities[cap].toFixed(1)}</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
function StageBar({ label, active, complete, progress }: {
label: string; active: boolean; complete: boolean; progress: number;
}) {
return (
<div className="flex-1">
<div className="text-[9px] text-center mb-0.5 text-surface-500">
{label}
</div>
<div className={`h-1 rounded-full ${
complete ? 'bg-success' :
active ? 'bg-accent' :
'bg-surface-700'
}`}>
{active && !complete && (
<div className="h-full bg-accent rounded-full transition-all" style={{ width: `${progress * 100}%` }} />
)}
</div>
</div>
);
}