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:
@@ -61,6 +61,19 @@ export function ToastContainer() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="text-sm font-medium">{toast.title}</div>
|
<div className="text-sm font-medium">{toast.title}</div>
|
||||||
<div className="text-xs text-surface-400">{toast.message}</div>
|
<div className="text-xs text-surface-400">{toast.message}</div>
|
||||||
|
{toast.action && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (toast.action!.page) useGameStore.getState().setActivePage(toast.action!.page);
|
||||||
|
if (toast.action!.modelsTab) useGameStore.getState().setModelsTab(toast.action!.modelsTab);
|
||||||
|
dismissNotification(toast.id);
|
||||||
|
setToasts(prev => prev.filter(t => t.id !== toast.id));
|
||||||
|
}}
|
||||||
|
className="text-xs text-accent hover:text-accent-light underline mt-1"
|
||||||
|
>
|
||||||
|
{toast.action.label}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function useGameLoop(skip = false) {
|
|||||||
message: n.message,
|
message: n.message,
|
||||||
type: n.type,
|
type: n.type,
|
||||||
tick: store.meta.tickCount,
|
tick: store.meta.tickCount,
|
||||||
|
...(n.action ? { action: n.action as any } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export function ModelsPage() {
|
|||||||
const openSourcedModels = useGameStore((s) => s.market.openSourcedModels);
|
const openSourcedModels = useGameStore((s) => s.market.openSourcedModels);
|
||||||
const completedResearch = useGameStore((s) => s.research.completedResearch);
|
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 [modelName, setModelName] = useState('');
|
||||||
const [expandedModel, setExpandedModel] = useState<string | null>(null);
|
const [expandedModel, setExpandedModel] = useState<string | null>(null);
|
||||||
const [expandedPipeline, setExpandedPipeline] = 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 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 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 eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'] as const;
|
||||||
const currentEraIdx = eraOrder.indexOf(currentEra);
|
const currentEraIdx = eraOrder.indexOf(currentEra);
|
||||||
@@ -135,9 +141,6 @@ export function ModelsPage() {
|
|||||||
r === 'alignment-research' || r === 'interpretability' || r === 'constitutional-ai',
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-2xl font-bold">Models</h2>
|
<h2 className="text-2xl font-bold">Models</h2>
|
||||||
@@ -157,13 +160,22 @@ export function ModelsPage() {
|
|||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setModelsTab(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
|
modelsTab === tab.id
|
||||||
? 'bg-surface-800 text-surface-100 border-b-2 border-accent'
|
? 'bg-surface-800 text-surface-100 border-b-2 border-accent'
|
||||||
: 'text-surface-400 hover:text-surface-200 hover:bg-surface-800/50'
|
: 'text-surface-400 hover:text-surface-200 hover:bg-surface-800/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -247,6 +259,19 @@ export function ModelsPage() {
|
|||||||
: `ETA: ${formatDuration(stage.totalTicks - stage.progressTicks)}`}
|
: `ETA: ${formatDuration(stage.totalTicks - stage.progressTicks)}`}
|
||||||
</div>
|
</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 && (
|
{isExpanded && (
|
||||||
<div className="mt-3 pt-3 border-t border-surface-700 space-y-2">
|
<div className="mt-3 pt-3 border-t border-surface-700 space-y-2">
|
||||||
{pipeline.currentStage === 'pretraining' && (
|
{pipeline.currentStage === 'pretraining' && (
|
||||||
@@ -256,8 +281,8 @@ export function ModelsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!pipeline.stages.sft && pipeline.stages.pretraining.progressTicks < pipeline.stages.pretraining.totalTicks * 0.5 && (
|
{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} />
|
<PostTrainingConfig pipelineId={pipeline.id} hasAlignmentResearch={hasAlignmentResearch} completedResearch={completedResearch} configureSFT={configureSFT} configureAlignment={configureAlignment} sftConfigured={!!pipeline.stages.sft} alignmentConfigured={!!pipeline.stages.alignment} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{recentEvents.length > 0 && (
|
{recentEvents.length > 0 && (
|
||||||
@@ -567,6 +592,14 @@ export function ModelsPage() {
|
|||||||
{/* Product Lines */}
|
{/* Product Lines */}
|
||||||
{modelsTab === 'products' && <div className="space-y-3">
|
{modelsTab === 'products' && <div className="space-y-3">
|
||||||
<h3 className="font-semibold">Product Lines</h3>
|
<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 => (
|
{productLines.map(pl => (
|
||||||
<div key={pl.id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
<div key={pl.id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -584,17 +617,28 @@ export function ModelsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
{/* Empty state for Models tab */}
|
|
||||||
{modelsTab === 'models' && families.length === 0 && (
|
{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">
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center space-y-3">
|
||||||
No model families yet. Train your first model from the Train New tab.
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty state for Overview when nothing is active */}
|
{modelsTab === 'overview' && !hasActiveJobs && (
|
||||||
{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 space-y-3">
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center text-surface-500 text-sm">
|
<p className="text-surface-400 text-sm">No active training or evaluation jobs.</p>
|
||||||
No active jobs. Start a training pipeline from the Train New tab.
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1069,8 +1113,15 @@ function StageBar({ label, active, complete, progress, configured = true }: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-[9px] text-center text-surface-500 mb-0.5">{label}</div>
|
<div className={`text-[9px] text-center mb-0.5 ${!configured ? 'text-warning' : 'text-surface-500'}`}>
|
||||||
<div className={`h-1 rounded-full ${!configured ? 'bg-surface-800' : complete ? 'bg-success' : active ? 'bg-accent' : 'bg-surface-700'}`}>
|
{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 && (
|
{active && !complete && (
|
||||||
<div className="h-full bg-accent rounded-full transition-all" style={{ width: `${progress * 100}%` }} />
|
<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;
|
pipelineId: string;
|
||||||
hasAlignmentResearch: boolean;
|
hasAlignmentResearch: boolean;
|
||||||
completedResearch: string[];
|
completedResearch: string[];
|
||||||
configureSFT: (pipelineId: string, specializations: SFTSpecialization[]) => void;
|
configureSFT: (pipelineId: string, specializations: SFTSpecialization[]) => void;
|
||||||
configureAlignment: (pipelineId: string, method: AlignmentMethod, safetyWeight: number) => void;
|
configureAlignment: (pipelineId: string, method: AlignmentMethod, safetyWeight: number) => void;
|
||||||
|
sftConfigured: boolean;
|
||||||
|
alignmentConfigured: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [selectedSpecs, setSelectedSpecs] = useState<SFTSpecialization[]>(['general']);
|
const [selectedSpecs, setSelectedSpecs] = useState<SFTSpecialization[]>(['general']);
|
||||||
const [alignMethod, setAlignMethod] = useState<AlignmentMethod>('rlhf');
|
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="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="text-xs font-medium text-surface-300">Configure Post-Training (optional)</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
{!sftConfigured ? (
|
||||||
<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 && (
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-1 text-xs text-surface-400">
|
<div className="flex items-center gap-1 text-xs text-surface-400">
|
||||||
<Shield size={10} /> Alignment
|
<Beaker size={10} /> Supervised Fine-Tuning
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{(Object.keys(ALIGNMENT_METHODS) as AlignmentMethod[]).map(method => {
|
{SFT_OPTIONS.map(opt => (
|
||||||
const isAvailable = completedResearch.includes(ALIGNMENT_METHODS[method].requiredResearch);
|
<button
|
||||||
return (
|
key={opt.value}
|
||||||
<button
|
onClick={() => setSelectedSpecs(prev =>
|
||||||
key={method}
|
prev.includes(opt.value)
|
||||||
disabled={!isAvailable}
|
? prev.filter(s => s !== opt.value)
|
||||||
onClick={() => setAlignMethod(method)}
|
: [...prev, opt.value]
|
||||||
className={`px-2 py-0.5 rounded text-[10px] border transition-colors ${
|
)}
|
||||||
alignMethod === method ? 'bg-accent/20 border-accent text-accent-light' :
|
className={`px-2 py-0.5 rounded text-[10px] border transition-colors ${
|
||||||
!isAvailable ? 'bg-surface-800 border-surface-700 text-surface-600 cursor-not-allowed' :
|
selectedSpecs.includes(opt.value)
|
||||||
'bg-surface-800 border-surface-600 text-surface-400'
|
? 'bg-accent/20 border-accent text-accent-light'
|
||||||
}`}
|
: 'bg-surface-800 border-surface-600 text-surface-400'
|
||||||
>
|
}`}
|
||||||
{method.toUpperCase()}
|
>
|
||||||
</button>
|
{opt.label}
|
||||||
);
|
</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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => configureAlignment(pipelineId, alignMethod, safetyWeight)}
|
onClick={() => configureSFT(pipelineId, selectedSpecs)}
|
||||||
className="text-[10px] text-accent hover:text-accent-light"
|
className="text-[10px] text-accent hover:text-accent-light"
|
||||||
>
|
>
|
||||||
Enable Alignment
|
Enable SFT
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+260
-190
@@ -58,10 +58,13 @@ export interface InfraNav {
|
|||||||
datacenterId?: string;
|
datacenterId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ModelsTab = 'overview' | 'train' | 'models' | 'benchmarks' | 'products';
|
||||||
|
|
||||||
interface UIState {
|
interface UIState {
|
||||||
activePage: ActivePage;
|
activePage: ActivePage;
|
||||||
notifications: GameNotification[];
|
notifications: GameNotification[];
|
||||||
infraNav: InfraNav;
|
infraNav: InfraNav;
|
||||||
|
modelsTab: ModelsTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameNotification {
|
export interface GameNotification {
|
||||||
@@ -71,6 +74,7 @@ export interface GameNotification {
|
|||||||
type: 'info' | 'success' | 'warning' | 'danger';
|
type: 'info' | 'success' | 'warning' | 'danger';
|
||||||
tick: number;
|
tick: number;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
|
action?: { label: string; page?: ActivePage; modelsTab?: ModelsTab };
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyDC(): Pick<DataCenter, 'networkSummary' | 'effectiveComputeRacks' | 'usedSlots' | 'usedPowerKW' | 'energyCostPerTick' | 'maintenanceCostPerTick' | 'currentUptime'> {
|
function emptyDC(): Pick<DataCenter, 'networkSummary' | 'effectiveComputeRacks' | 'usedSlots' | 'usedPowerKW' | 'energyCostPerTick' | 'maintenanceCostPerTick' | 'currentUptime'> {
|
||||||
@@ -86,6 +90,7 @@ function emptyDC(): Pick<DataCenter, 'networkSummary' | 'effectiveComputeRacks'
|
|||||||
interface Actions {
|
interface Actions {
|
||||||
setActivePage: (page: ActivePage) => void;
|
setActivePage: (page: ActivePage) => void;
|
||||||
setInfraNav: (nav: InfraNav) => void;
|
setInfraNav: (nav: InfraNav) => void;
|
||||||
|
setModelsTab: (tab: ModelsTab) => void;
|
||||||
addNotification: (n: Omit<GameNotification, 'id' | 'read'>) => void;
|
addNotification: (n: Omit<GameNotification, 'id' | 'read'>) => void;
|
||||||
dismissNotification: (id: string) => void;
|
dismissNotification: (id: string) => void;
|
||||||
removeNotification: (id: string) => void;
|
removeNotification: (id: string) => void;
|
||||||
@@ -289,11 +294,14 @@ export const useGameStore = create<Store>()(
|
|||||||
activePage: 'dashboard' as ActivePage,
|
activePage: 'dashboard' as ActivePage,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
infraNav: { level: 'clusters' } as InfraNav,
|
infraNav: { level: 'clusters' } as InfraNav,
|
||||||
|
modelsTab: 'overview' as ModelsTab,
|
||||||
|
|
||||||
setActivePage: (page) => set({ activePage: page }),
|
setActivePage: (page) => set({ activePage: page }),
|
||||||
|
|
||||||
setInfraNav: (nav) => set({ infraNav: nav }),
|
setInfraNav: (nav) => set({ infraNav: nav }),
|
||||||
|
|
||||||
|
setModelsTab: (tab) => set({ modelsTab: tab }),
|
||||||
|
|
||||||
addNotification: (n) => set((s) => ({
|
addNotification: (n) => set((s) => ({
|
||||||
notifications: [
|
notifications: [
|
||||||
{ ...n, id: uuid(), read: false },
|
{ ...n, id: uuid(), read: false },
|
||||||
@@ -907,197 +915,252 @@ export const useGameStore = create<Store>()(
|
|||||||
|
|
||||||
// --- Non-infrastructure actions (unchanged) ---
|
// --- Non-infrastructure actions (unchanged) ---
|
||||||
|
|
||||||
startTrainingPipeline: (config) => set((s) => {
|
startTrainingPipeline: (config) => {
|
||||||
const activeCount = s.models.activeTrainingPipelines.filter(p => p.status === 'active' || p.status === 'stalled').length;
|
let created = false;
|
||||||
const maxSlots = MAX_CONCURRENT_TRAINING[s.meta.currentEra] ?? 1;
|
set((s) => {
|
||||||
if (activeCount >= maxSlots) return s;
|
const activeCount = s.models.activeTrainingPipelines.filter(p => p.status === 'active' || p.status === 'stalled').length;
|
||||||
|
const maxSlots = MAX_CONCURRENT_TRAINING[s.meta.currentEra] ?? 1;
|
||||||
|
if (activeCount >= maxSlots) return s;
|
||||||
|
|
||||||
const familyId = uuid();
|
created = true;
|
||||||
const pipelineId = uuid();
|
const familyId = uuid();
|
||||||
const generation = s.models.families.length + 1;
|
const pipelineId = uuid();
|
||||||
|
const generation = s.models.families.length + 1;
|
||||||
|
|
||||||
const family: ModelFamily = {
|
const family: ModelFamily = {
|
||||||
id: familyId,
|
id: familyId,
|
||||||
name: config.modelName,
|
name: config.modelName,
|
||||||
generation,
|
generation,
|
||||||
baseModelId: null,
|
baseModelId: null,
|
||||||
variants: [],
|
variants: [],
|
||||||
createdAtTick: s.meta.tickCount,
|
createdAtTick: s.meta.tickCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
const pipeline: TrainingPipeline = {
|
const pipeline: TrainingPipeline = {
|
||||||
id: pipelineId,
|
id: pipelineId,
|
||||||
familyId,
|
familyId,
|
||||||
modelName: config.modelName,
|
modelName: config.modelName,
|
||||||
architecture: config.architecture,
|
architecture: config.architecture,
|
||||||
dataMix: config.dataMix,
|
dataMix: config.dataMix,
|
||||||
currentStage: 'pretraining',
|
currentStage: 'pretraining',
|
||||||
stages: {
|
stages: {
|
||||||
pretraining: {
|
pretraining: {
|
||||||
targetTokens: config.targetTokens,
|
targetTokens: config.targetTokens,
|
||||||
processedTokens: 0,
|
processedTokens: 0,
|
||||||
computeAllocated: 0,
|
computeAllocated: 0,
|
||||||
progressTicks: 0,
|
progressTicks: 0,
|
||||||
totalTicks: config.totalTicks,
|
totalTicks: config.totalTicks,
|
||||||
lossValue: 10,
|
lossValue: 10,
|
||||||
chinchillaRatio: config.targetTokens / (config.architecture.totalParameters * 1e9),
|
chinchillaRatio: config.targetTokens / (config.architecture.totalParameters * 1e9),
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
|
},
|
||||||
|
sft: null,
|
||||||
|
alignment: null,
|
||||||
},
|
},
|
||||||
sft: null,
|
status: 'active',
|
||||||
alignment: null,
|
allocatedComputeFraction: config.allocatedComputeFraction,
|
||||||
},
|
events: [],
|
||||||
status: 'active',
|
startedAtTick: s.meta.tickCount,
|
||||||
allocatedComputeFraction: config.allocatedComputeFraction,
|
};
|
||||||
events: [],
|
|
||||||
startedAtTick: s.meta.tickCount,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
models: {
|
||||||
|
...s.models,
|
||||||
|
families: [...s.models.families, family],
|
||||||
|
activeTrainingPipelines: [...s.models.activeTrainingPipelines, pipeline],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (created) {
|
||||||
|
get().addNotification({ title: 'Training Started', message: `${config.modelName} pre-training has begun.`, type: 'info', tick: get().meta.tickCount });
|
||||||
|
set({ modelsTab: 'overview' as ModelsTab });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
configureSFT: (pipelineId, specializations) => {
|
||||||
|
set((s) => ({
|
||||||
models: {
|
models: {
|
||||||
...s.models,
|
...s.models,
|
||||||
families: [...s.models.families, family],
|
activeTrainingPipelines: s.models.activeTrainingPipelines.map(p =>
|
||||||
activeTrainingPipelines: [...s.models.activeTrainingPipelines, pipeline],
|
p.id === pipelineId ? {
|
||||||
|
...p,
|
||||||
|
stages: {
|
||||||
|
...p.stages,
|
||||||
|
sft: {
|
||||||
|
specializations,
|
||||||
|
progressTicks: 0,
|
||||||
|
totalTicks: Math.ceil(p.stages.pretraining.totalTicks * 0.10),
|
||||||
|
isComplete: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} : p,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
};
|
}));
|
||||||
}),
|
get().addNotification({ title: 'SFT Configured', message: `${specializations.join(', ')} specializations enabled.`, type: 'success', tick: get().meta.tickCount });
|
||||||
|
},
|
||||||
|
|
||||||
configureSFT: (pipelineId, specializations) => set((s) => ({
|
configureAlignment: (pipelineId, method, safetyWeight) => {
|
||||||
models: {
|
set((s) => ({
|
||||||
...s.models,
|
models: {
|
||||||
activeTrainingPipelines: s.models.activeTrainingPipelines.map(p =>
|
...s.models,
|
||||||
p.id === pipelineId ? {
|
activeTrainingPipelines: s.models.activeTrainingPipelines.map(p =>
|
||||||
...p,
|
p.id === pipelineId ? {
|
||||||
stages: {
|
...p,
|
||||||
...p.stages,
|
stages: {
|
||||||
sft: {
|
...p.stages,
|
||||||
specializations,
|
alignment: {
|
||||||
progressTicks: 0,
|
method,
|
||||||
totalTicks: Math.ceil(p.stages.pretraining.totalTicks * 0.10),
|
safetyWeight,
|
||||||
isComplete: false,
|
helpfulnessWeight: 1 - safetyWeight,
|
||||||
|
progressTicks: 0,
|
||||||
|
totalTicks: Math.ceil(p.stages.pretraining.totalTicks * 0.08),
|
||||||
|
isComplete: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
} : p,
|
||||||
} : p,
|
),
|
||||||
),
|
},
|
||||||
},
|
}));
|
||||||
})),
|
get().addNotification({ title: 'Alignment Configured', message: `${method.toUpperCase()} alignment enabled.`, type: 'success', tick: get().meta.tickCount });
|
||||||
|
},
|
||||||
|
|
||||||
configureAlignment: (pipelineId, method, safetyWeight) => set((s) => ({
|
createDistillation: (baseModelId, targetParameters, variantName) => {
|
||||||
models: {
|
let created = false;
|
||||||
...s.models,
|
set((s) => {
|
||||||
activeTrainingPipelines: s.models.activeTrainingPipelines.map(p =>
|
const base = s.models.baseModels.find(m => m.id === baseModelId);
|
||||||
p.id === pipelineId ? {
|
if (!base) return s;
|
||||||
...p,
|
created = true;
|
||||||
stages: {
|
const job: VariantCreationJob = {
|
||||||
...p.stages,
|
id: uuid(),
|
||||||
alignment: {
|
familyId: base.familyId,
|
||||||
method,
|
baseModelId,
|
||||||
safetyWeight,
|
jobType: 'distillation',
|
||||||
helpfulnessWeight: 1 - safetyWeight,
|
config: { targetParameters, targetArchitecture: base.architecture.type, variantName },
|
||||||
progressTicks: 0,
|
progressTicks: 0,
|
||||||
totalTicks: Math.ceil(p.stages.pretraining.totalTicks * 0.08),
|
totalTicks: Math.ceil(base.trainingCostTotal > 0 ? DISTILLATION_TIME_FRACTION * 120 : 30),
|
||||||
isComplete: false,
|
allocatedComputeFraction: DISTILLATION_COMPUTE_FRACTION,
|
||||||
},
|
status: 'active',
|
||||||
},
|
};
|
||||||
} : p,
|
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
|
||||||
),
|
});
|
||||||
},
|
if (created) {
|
||||||
})),
|
get().addNotification({ title: 'Distillation Started', message: `${variantName} distillation in progress.`, type: 'info', tick: get().meta.tickCount });
|
||||||
|
set({ modelsTab: 'overview' as ModelsTab });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
createDistillation: (baseModelId, targetParameters, variantName) => set((s) => {
|
createFineTune: (baseModelId, specialization, variantName) => {
|
||||||
const base = s.models.baseModels.find(m => m.id === baseModelId);
|
let created = false;
|
||||||
if (!base) return s;
|
set((s) => {
|
||||||
const job: VariantCreationJob = {
|
const base = s.models.baseModels.find(m => m.id === baseModelId);
|
||||||
id: uuid(),
|
if (!base) return s;
|
||||||
familyId: base.familyId,
|
created = true;
|
||||||
baseModelId,
|
const job: VariantCreationJob = {
|
||||||
jobType: 'distillation',
|
id: uuid(),
|
||||||
config: { targetParameters, targetArchitecture: base.architecture.type, variantName },
|
familyId: base.familyId,
|
||||||
progressTicks: 0,
|
baseModelId,
|
||||||
totalTicks: Math.ceil(base.trainingCostTotal > 0 ? DISTILLATION_TIME_FRACTION * 120 : 30),
|
jobType: 'fine-tuning',
|
||||||
allocatedComputeFraction: DISTILLATION_COMPUTE_FRACTION,
|
config: { specialization, datasetIds: [], variantName },
|
||||||
status: 'active',
|
progressTicks: 0,
|
||||||
};
|
totalTicks: Math.ceil(FINETUNE_TIME_FRACTION * 120),
|
||||||
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
|
allocatedComputeFraction: FINETUNE_COMPUTE_FRACTION,
|
||||||
}),
|
status: 'active',
|
||||||
|
};
|
||||||
|
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
|
||||||
|
});
|
||||||
|
if (created) {
|
||||||
|
get().addNotification({ title: 'Fine-Tuning Started', message: `${variantName} fine-tuning in progress.`, type: 'info', tick: get().meta.tickCount });
|
||||||
|
set({ modelsTab: 'overview' as ModelsTab });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
createFineTune: (baseModelId, specialization, variantName) => set((s) => {
|
createQuantization: (baseModelId, level, variantName) => {
|
||||||
const base = s.models.baseModels.find(m => m.id === baseModelId);
|
let created = false;
|
||||||
if (!base) return s;
|
set((s) => {
|
||||||
const job: VariantCreationJob = {
|
const base = s.models.baseModels.find(m => m.id === baseModelId);
|
||||||
id: uuid(),
|
if (!base) return s;
|
||||||
familyId: base.familyId,
|
created = true;
|
||||||
baseModelId,
|
const job: VariantCreationJob = {
|
||||||
jobType: 'fine-tuning',
|
id: uuid(),
|
||||||
config: { specialization, datasetIds: [], variantName },
|
familyId: base.familyId,
|
||||||
progressTicks: 0,
|
baseModelId,
|
||||||
totalTicks: Math.ceil(FINETUNE_TIME_FRACTION * 120),
|
jobType: 'quantization',
|
||||||
allocatedComputeFraction: FINETUNE_COMPUTE_FRACTION,
|
config: { level, variantName },
|
||||||
status: 'active',
|
progressTicks: 0,
|
||||||
};
|
totalTicks: QUANTIZATION_TICKS,
|
||||||
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
|
allocatedComputeFraction: 0,
|
||||||
}),
|
status: 'active',
|
||||||
|
};
|
||||||
|
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
|
||||||
|
});
|
||||||
|
if (created) {
|
||||||
|
get().addNotification({ title: 'Quantization Started', message: `${variantName} quantization in progress.`, type: 'info', tick: get().meta.tickCount });
|
||||||
|
set({ modelsTab: 'overview' as ModelsTab });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
createQuantization: (baseModelId, level, variantName) => set((s) => {
|
startEvaluation: (modelId, benchmarkIds) => {
|
||||||
const base = s.models.baseModels.find(m => m.id === baseModelId);
|
let created = false;
|
||||||
if (!base) return s;
|
set((s) => {
|
||||||
const job: VariantCreationJob = {
|
const benchmarks = BENCHMARKS.filter(b => benchmarkIds.includes(b.id));
|
||||||
id: uuid(),
|
if (benchmarks.length === 0) return s;
|
||||||
familyId: base.familyId,
|
created = true;
|
||||||
baseModelId,
|
const totalTicks = benchmarks.reduce((sum, b) => sum + b.ticksToRun, 0);
|
||||||
jobType: 'quantization',
|
const computeCost = benchmarks.reduce((sum, b) => sum + b.computeCost, 0);
|
||||||
config: { level, variantName },
|
const job: EvalJob = {
|
||||||
progressTicks: 0,
|
id: uuid(),
|
||||||
totalTicks: QUANTIZATION_TICKS,
|
modelId,
|
||||||
allocatedComputeFraction: 0,
|
benchmarkIds,
|
||||||
status: 'active',
|
progressTicks: 0,
|
||||||
};
|
totalTicks,
|
||||||
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
|
computeAllocated: computeCost,
|
||||||
}),
|
status: 'active',
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
return { models: { ...s.models, evalJobs: [...s.models.evalJobs, job] } };
|
||||||
|
});
|
||||||
|
if (created) {
|
||||||
|
get().addNotification({ title: 'Evaluation Started', message: `${benchmarkIds.length} benchmark${benchmarkIds.length > 1 ? 's' : ''} queued.`, type: 'info', tick: get().meta.tickCount });
|
||||||
|
set({ modelsTab: 'overview' as ModelsTab });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
startEvaluation: (modelId, benchmarkIds) => set((s) => {
|
deployModel: (modelId) => {
|
||||||
const benchmarks = BENCHMARKS.filter(b => benchmarkIds.includes(b.id));
|
const modelName = get().models.baseModels.find(m => m.id === modelId)?.name ?? 'Model';
|
||||||
if (benchmarks.length === 0) return s;
|
set((s) => ({
|
||||||
const totalTicks = benchmarks.reduce((sum, b) => sum + b.ticksToRun, 0);
|
models: {
|
||||||
const computeCost = benchmarks.reduce((sum, b) => sum + b.computeCost, 0);
|
...s.models,
|
||||||
const job: EvalJob = {
|
baseModels: s.models.baseModels.map(m =>
|
||||||
id: uuid(),
|
m.id === modelId ? { ...m, isDeployed: true } : m,
|
||||||
modelId,
|
),
|
||||||
benchmarkIds,
|
productLines: s.models.productLines.map(pl => ({
|
||||||
progressTicks: 0,
|
...pl, modelId, isActive: true,
|
||||||
totalTicks,
|
})),
|
||||||
computeAllocated: computeCost,
|
},
|
||||||
status: 'active',
|
market: {
|
||||||
results: [],
|
...s.market,
|
||||||
};
|
obsolescence: onModelDeployed(s.market.obsolescence, s.meta.tickCount),
|
||||||
return { models: { ...s.models, evalJobs: [...s.models.evalJobs, job] } };
|
},
|
||||||
}),
|
}));
|
||||||
|
get().addNotification({ title: 'Model Deployed', message: `${modelName} is now serving all product lines.`, type: 'success', tick: get().meta.tickCount });
|
||||||
|
set({ modelsTab: 'products' as ModelsTab });
|
||||||
|
},
|
||||||
|
|
||||||
deployModel: (modelId) => set((s) => ({
|
deployVariant: (familyId, variantId) => {
|
||||||
models: {
|
set((s) => ({
|
||||||
...s.models,
|
models: {
|
||||||
baseModels: s.models.baseModels.map(m =>
|
...s.models,
|
||||||
m.id === modelId ? { ...m, isDeployed: true } : m,
|
families: s.models.families.map(f =>
|
||||||
),
|
f.id === familyId
|
||||||
productLines: s.models.productLines.map(pl => ({
|
? { ...f, variants: f.variants.map(v => v.id === variantId ? { ...v, isDeployed: true } : v) }
|
||||||
...pl, modelId, isActive: true,
|
: f,
|
||||||
})),
|
),
|
||||||
},
|
},
|
||||||
market: {
|
}));
|
||||||
...s.market,
|
get().addNotification({ title: 'Variant Deployed', message: 'Variant is now live.', type: 'success', tick: get().meta.tickCount });
|
||||||
obsolescence: onModelDeployed(s.market.obsolescence, s.meta.tickCount),
|
set({ modelsTab: 'products' as ModelsTab });
|
||||||
},
|
},
|
||||||
})),
|
|
||||||
|
|
||||||
deployVariant: (familyId, variantId) => set((s) => ({
|
|
||||||
models: {
|
|
||||||
...s.models,
|
|
||||||
families: s.models.families.map(f =>
|
|
||||||
f.id === familyId
|
|
||||||
? { ...f, variants: f.variants.map(v => v.id === variantId ? { ...v, isDeployed: true } : v) }
|
|
||||||
: f,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
|
|
||||||
setProductPricing: (productLineId, field, value) => set((s) => ({
|
setProductPricing: (productLineId, field, value) => set((s) => ({
|
||||||
models: {
|
models: {
|
||||||
@@ -1188,20 +1251,27 @@ export const useGameStore = create<Store>()(
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
openSourceModel: (modelId) => set((s) => {
|
openSourceModel: (modelId) => {
|
||||||
if (s.market.openSourcedModels.includes(modelId)) return s;
|
let opened = false;
|
||||||
return {
|
set((s) => {
|
||||||
market: {
|
if (s.market.openSourcedModels.includes(modelId)) return s;
|
||||||
...s.market,
|
opened = true;
|
||||||
openSourcedModels: [...s.market.openSourcedModels, modelId],
|
return {
|
||||||
},
|
market: {
|
||||||
reputation: {
|
...s.market,
|
||||||
...s.reputation,
|
openSourcedModels: [...s.market.openSourcedModels, modelId],
|
||||||
score: Math.min(100, s.reputation.score + OPEN_SOURCE_REPUTATION_BOOST),
|
},
|
||||||
publicPerception: Math.min(100, s.reputation.publicPerception + OPEN_SOURCE_REPUTATION_BOOST),
|
reputation: {
|
||||||
},
|
...s.reputation,
|
||||||
};
|
score: Math.min(100, s.reputation.score + OPEN_SOURCE_REPUTATION_BOOST),
|
||||||
}),
|
publicPerception: Math.min(100, s.reputation.publicPerception + OPEN_SOURCE_REPUTATION_BOOST),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (opened) {
|
||||||
|
get().addNotification({ title: 'Model Open Sourced', message: 'Reputation boosted! Competitors may benefit.', type: 'success', tick: get().meta.tickCount });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setOverloadPolicy: (policy) => set((s) => ({
|
setOverloadPolicy: (policy) => set((s) => ({
|
||||||
market: {
|
market: {
|
||||||
@@ -1382,7 +1452,7 @@ export const useGameStore = create<Store>()(
|
|||||||
name: 'ai-tycoon-save',
|
name: 'ai-tycoon-save',
|
||||||
version: SAVE_VERSION,
|
version: SAVE_VERSION,
|
||||||
partialize: (state) => {
|
partialize: (state) => {
|
||||||
const { activePage, notifications, infraNav, ...rest } = state;
|
const { activePage, notifications, infraNav, modelsTab, ...rest } = state;
|
||||||
return rest;
|
return rest;
|
||||||
},
|
},
|
||||||
migrate: (_persisted, version) => {
|
migrate: (_persisted, version) => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import type { ResearchBonuses } from './researchBonuses';
|
|||||||
export interface ModelTickResult {
|
export interface ModelTickResult {
|
||||||
modelsState: ModelsState;
|
modelsState: ModelsState;
|
||||||
completedModels: BaseModel[];
|
completedModels: BaseModel[];
|
||||||
notifications: { title: string; message: string; type: 'success' | 'warning' | 'info' | 'danger' }[];
|
notifications: { title: string; message: string; type: 'success' | 'warning' | 'info' | 'danger'; action?: { label: string; page?: string; modelsTab?: string } }[];
|
||||||
reputationHit: number;
|
reputationHit: number;
|
||||||
legalCosts: number;
|
legalCosts: number;
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,16 @@ export function processModels(state: GameState, researchBonuses?: ResearchBonuse
|
|||||||
stage.computeAllocated = effectiveFlops;
|
stage.computeAllocated = effectiveFlops;
|
||||||
stage.lossValue = Math.max(0.01, 10 * Math.exp(-stage.progressTicks / stage.totalTicks * 3));
|
stage.lossValue = Math.max(0.01, 10 * Math.exp(-stage.progressTicks / stage.totalTicks * 3));
|
||||||
|
|
||||||
|
const progressRatio = stage.progressTicks / stage.totalTicks;
|
||||||
|
if (progressRatio >= 0.75 && progressRatio < 0.78 && !pipeline.stages.sft && !pipeline.stages.alignment) {
|
||||||
|
notifications.push({
|
||||||
|
title: 'Post-Training Reminder',
|
||||||
|
message: `${pipeline.modelName} is 75% pre-trained. Configure SFT/Alignment now or they'll be skipped!`,
|
||||||
|
type: 'warning',
|
||||||
|
action: { label: 'Configure Now', page: 'models', modelsTab: 'overview' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (stage.progressTicks >= stage.totalTicks) {
|
if (stage.progressTicks >= stage.totalTicks) {
|
||||||
stage.isComplete = true;
|
stage.isComplete = true;
|
||||||
stage.progressTicks = stage.totalTicks;
|
stage.progressTicks = stage.totalTicks;
|
||||||
@@ -174,8 +184,9 @@ export function processModels(state: GameState, researchBonuses?: ResearchBonuse
|
|||||||
);
|
);
|
||||||
notifications.push({
|
notifications.push({
|
||||||
title: 'Variant Created',
|
title: 'Variant Created',
|
||||||
message: `${variant.name} (${variant.variantType}) is ready!`,
|
message: `${variant.name} (${variant.variantType}) is ready! Deploy it to use it.`,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
action: { label: 'View in Families', page: 'models', modelsTab: 'models' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export interface TickNotification {
|
|||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
type: 'info' | 'success' | 'warning' | 'danger';
|
type: 'info' | 'success' | 'warning' | 'danger';
|
||||||
|
action?: { label: string; page?: string; modelsTab?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedAchievementDefs: AchievementDefinition[] | null = null;
|
let cachedAchievementDefs: AchievementDefinition[] | null = null;
|
||||||
@@ -45,8 +46,9 @@ export function processTick(state: GameState): Partial<GameState> {
|
|||||||
for (const completed of modelResult.completedModels) {
|
for (const completed of modelResult.completedModels) {
|
||||||
notifications.push({
|
notifications.push({
|
||||||
title: 'Training Complete',
|
title: 'Training Complete',
|
||||||
message: `${completed.name} is ready! Capability: ${completed.rawCapability.toFixed(1)}/100`,
|
message: `${completed.name} is ready! Capability: ${completed.rawCapability.toFixed(1)}/100. Deploy it to start earning revenue.`,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
action: { label: 'Go to Families', page: 'models', modelsTab: 'models' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
notifications.push(...modelResult.notifications);
|
notifications.push(...modelResult.notifications);
|
||||||
|
|||||||
Reference in New Issue
Block a user