Overhaul Models tab UX: action feedback, post-training flow, guided navigation
CI / build-and-push (push) Successful in 29s
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:
@@ -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 & 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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user