Overhaul Models tab UX: action feedback, post-training flow, guided navigation
CI / build-and-push (push) Successful in 29s

- Lift modelsTab state into Zustand store so actions can navigate tabs
- Add toast notifications + auto-tab-switch to all 10 model actions
  (train, configure SFT/alignment, distill, fine-tune, quantize, eval, deploy, open-source)
- Add actionable toast buttons with navigation (e.g., "Go to Families" on training complete)
- Fix post-training config: remove 50% deadline, show until pretraining completes,
  always-visible warning prompt outside card expand, engine reminder at 75%
- PostTrainingConfig now hides already-configured sections independently
- Add tab badges: pulsing dot for active jobs, count for undeployed models, warning for no deployment
- Replace empty states with actionable buttons guiding next steps
- Stage bars show "(skip)" in warning color for unconfigured SFT/Alignment stages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 10:20:00 -04:00
parent fdedd6f4d0
commit 775c6a4fa5
6 changed files with 436 additions and 270 deletions
+146 -77
View File
@@ -69,7 +69,8 @@ export function ModelsPage() {
const openSourcedModels = useGameStore((s) => s.market.openSourcedModels);
const completedResearch = useGameStore((s) => s.research.completedResearch);
const [modelsTab, setModelsTab] = useState<'overview' | 'train' | 'models' | 'benchmarks' | 'products'>('overview');
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);
@@ -84,6 +85,11 @@ export function ModelsPage() {
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 activeEvalJobs = evalJobs.filter(j => j.status === 'active');
const undeployedCount = baseModels.filter(m => !m.isDeployed).length;
const hasActiveJobs = activePipelines.length > 0 || activeVariantJobs.length > 0 || activeEvalJobs.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);
@@ -135,9 +141,6 @@ export function ModelsPage() {
r === 'alignment-research' || r === 'interpretability' || r === 'constitutional-ai',
);
const activeVariantJobs = variantJobs.filter(j => j.status === 'active');
const activeEvalJobs = evalJobs.filter(j => j.status === 'active');
return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Models</h2>
@@ -157,13 +160,22 @@ export function ModelsPage() {
<button
key={tab.id}
onClick={() => setModelsTab(tab.id)}
className={`px-4 py-2 text-sm rounded-t-lg transition-colors ${
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>
@@ -247,6 +259,19 @@ export function ModelsPage() {
: `ETA: ${formatDuration(stage.totalTicks - stage.progressTicks)}`}
</div>
{pipeline.currentStage === 'pretraining' && !pipeline.stages.sft && !pipeline.stages.alignment && (
<div className="mt-2 flex items-center gap-2 text-xs text-warning">
<Beaker size={12} />
<span>
Post-training not configured {' '}
<button onClick={() => setExpandedPipeline(pipeline.id)} className="text-accent hover:text-accent-light underline">
configure SFT &amp; Alignment
</button>
{' '}or they'll be skipped.
</span>
</div>
)}
{isExpanded && (
<div className="mt-3 pt-3 border-t border-surface-700 space-y-2">
{pipeline.currentStage === 'pretraining' && (
@@ -256,8 +281,8 @@ export function ModelsPage() {
</div>
)}
{!pipeline.stages.sft && pipeline.stages.pretraining.progressTicks < pipeline.stages.pretraining.totalTicks * 0.5 && (
<PostTrainingConfig pipelineId={pipeline.id} hasAlignmentResearch={hasAlignmentResearch} completedResearch={completedResearch} configureSFT={configureSFT} configureAlignment={configureAlignment} />
{pipeline.currentStage === 'pretraining' && !pipeline.stages.pretraining.isComplete && (!pipeline.stages.sft || !pipeline.stages.alignment) && (
<PostTrainingConfig pipelineId={pipeline.id} hasAlignmentResearch={hasAlignmentResearch} completedResearch={completedResearch} configureSFT={configureSFT} configureAlignment={configureAlignment} sftConfigured={!!pipeline.stages.sft} alignmentConfigured={!!pipeline.stages.alignment} />
)}
{recentEvents.length > 0 && (
@@ -567,6 +592,14 @@ export function ModelsPage() {
{/* 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">
@@ -584,17 +617,28 @@ export function ModelsPage() {
))}
</div>}
{/* Empty state for Models tab */}
{modelsTab === 'models' && families.length === 0 && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center text-surface-500 text-sm">
No model families yet. Train your first model from the Train New tab.
<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>
)}
{/* Empty state for Overview when nothing is active */}
{modelsTab === 'overview' && activePipelines.length === 0 && activeVariantJobs.length === 0 && activeEvalJobs.length === 0 && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center text-surface-500 text-sm">
No active jobs. Start a training pipeline from the Train New tab.
{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>
@@ -1069,8 +1113,15 @@ function StageBar({ label, active, complete, progress, configured = true }: {
}) {
return (
<div className="flex-1">
<div className="text-[9px] text-center text-surface-500 mb-0.5">{label}</div>
<div className={`h-1 rounded-full ${!configured ? 'bg-surface-800' : complete ? 'bg-success' : active ? 'bg-accent' : 'bg-surface-700'}`}>
<div className={`text-[9px] text-center mb-0.5 ${!configured ? 'text-warning' : 'text-surface-500'}`}>
{label}{!configured && ' (skip)'}
</div>
<div className={`h-1 rounded-full ${
!configured ? 'bg-surface-800 border border-dashed border-warning/30' :
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}%` }} />
)}
@@ -1079,12 +1130,14 @@ function StageBar({ label, active, complete, progress, configured = true }: {
);
}
function PostTrainingConfig({ pipelineId, hasAlignmentResearch, completedResearch, configureSFT, configureAlignment }: {
function PostTrainingConfig({ pipelineId, hasAlignmentResearch, completedResearch, configureSFT, configureAlignment, sftConfigured, alignmentConfigured }: {
pipelineId: string;
hasAlignmentResearch: boolean;
completedResearch: string[];
configureSFT: (pipelineId: string, specializations: SFTSpecialization[]) => void;
configureAlignment: (pipelineId: string, method: AlignmentMethod, safetyWeight: number) => void;
sftConfigured: boolean;
alignmentConfigured: boolean;
}) {
const [selectedSpecs, setSelectedSpecs] = useState<SFTSpecialization[]>(['general']);
const [alignMethod, setAlignMethod] = useState<AlignmentMethod>('rlhf');
@@ -1094,75 +1147,91 @@ function PostTrainingConfig({ pipelineId, hasAlignmentResearch, completedResearc
<div className="space-y-3 bg-surface-800/50 rounded-lg p-3">
<div className="text-xs font-medium text-surface-300">Configure Post-Training (optional)</div>
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-surface-400">
<Beaker size={10} /> Supervised Fine-Tuning
</div>
<div className="flex flex-wrap gap-1">
{SFT_OPTIONS.map(opt => (
<button
key={opt.value}
onClick={() => setSelectedSpecs(prev =>
prev.includes(opt.value)
? prev.filter(s => s !== opt.value)
: [...prev, opt.value]
)}
className={`px-2 py-0.5 rounded text-[10px] border transition-colors ${
selectedSpecs.includes(opt.value)
? 'bg-accent/20 border-accent text-accent-light'
: 'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{opt.label}
</button>
))}
</div>
<button
onClick={() => configureSFT(pipelineId, selectedSpecs)}
className="text-[10px] text-accent hover:text-accent-light"
>
Enable SFT
</button>
</div>
{hasAlignmentResearch && (
{!sftConfigured ? (
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-surface-400">
<Shield size={10} /> Alignment
<Beaker size={10} /> Supervised Fine-Tuning
</div>
<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-0.5 rounded text-[10px] 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>
<div className="flex flex-wrap gap-1">
{SFT_OPTIONS.map(opt => (
<button
key={opt.value}
onClick={() => setSelectedSpecs(prev =>
prev.includes(opt.value)
? prev.filter(s => s !== opt.value)
: [...prev, opt.value]
)}
className={`px-2 py-0.5 rounded text-[10px] border transition-colors ${
selectedSpecs.includes(opt.value)
? 'bg-accent/20 border-accent text-accent-light'
: 'bg-surface-800 border-surface-600 text-surface-400'
}`}
>
{opt.label}
</button>
))}
</div>
<button
onClick={() => configureAlignment(pipelineId, alignMethod, safetyWeight)}
onClick={() => configureSFT(pipelineId, selectedSpecs)}
className="text-[10px] text-accent hover:text-accent-light"
>
Enable Alignment
Enable SFT
</button>
</div>
) : (
<div className="flex items-center gap-1 text-xs text-success">
<Beaker size={10} /> SFT configured
</div>
)}
{!alignmentConfigured ? (
hasAlignmentResearch ? (
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-surface-400">
<Shield size={10} /> Alignment
</div>
<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-0.5 rounded text-[10px] 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>
</div>
<button
onClick={() => configureAlignment(pipelineId, alignMethod, safetyWeight)}
className="text-[10px] text-accent hover:text-accent-light"
>
Enable Alignment
</button>
</div>
) : (
<div className="flex items-center gap-1 text-xs text-surface-500">
<Shield size={10} /> Alignment requires research
</div>
)
) : (
<div className="flex items-center gap-1 text-xs text-success">
<Shield size={10} /> Alignment configured
</div>
)}
</div>
);