Overhaul market system with shared TAM competition, multi-tier pricing, enterprise pipeline, and developer ecosystem
CI / build-and-push (push) Successful in 42s
CI / build-and-push (push) Successful in 42s
Replaces the simplified single-subscriber market with a full competitive simulation: shared TAM with softmax market shares across 4 segments, multi-tier consumer subscriptions (Free/Plus/Pro/Team) and API tiers (Free/PAYG/Scale/Enterprise), enterprise sales pipeline (Lead→Qualification→POC→Negotiation→Active→Renewal) with SLA tracking, developer ecosystem flywheel, technology obsolescence pressure, seasonal demand cycles, and two new product lines (Code Assistant, AI Agents Platform). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { useGameStore } from '@/store';
|
||||
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
|
||||
import type { EnterprisePipelineStage, EnterpriseSegment } from '@ai-tycoon/shared';
|
||||
import { Building2, AlertTriangle } from 'lucide-react';
|
||||
|
||||
const STAGE_ORDER: EnterprisePipelineStage[] = ['lead', 'qualification', 'poc', 'negotiation'];
|
||||
const STAGE_LABELS: Record<EnterprisePipelineStage, string> = {
|
||||
lead: 'Leads',
|
||||
qualification: 'Qualification',
|
||||
poc: 'POC',
|
||||
negotiation: 'Negotiation',
|
||||
};
|
||||
const STAGE_COLORS: Record<EnterprisePipelineStage, string> = {
|
||||
lead: 'bg-surface-600',
|
||||
qualification: 'bg-blue-600',
|
||||
poc: 'bg-purple-600',
|
||||
negotiation: 'bg-orange-600',
|
||||
};
|
||||
|
||||
const SEGMENT_BADGES: Record<EnterpriseSegment, { label: string; color: string }> = {
|
||||
startup: { label: 'Startup', color: 'bg-green-500/20 text-green-400' },
|
||||
'mid-market': { label: 'Mid-Market', color: 'bg-blue-500/20 text-blue-400' },
|
||||
enterprise: { label: 'Enterprise', color: 'bg-purple-500/20 text-purple-400' },
|
||||
government: { label: 'Gov', color: 'bg-yellow-500/20 text-yellow-400' },
|
||||
};
|
||||
|
||||
export function EnterprisePipelinePanel() {
|
||||
const enterprise = useGameStore((s) => s.market.enterprise);
|
||||
const tickCount = useGameStore((s) => s.meta.tickCount);
|
||||
|
||||
const leadsByStage = STAGE_ORDER.map(stage => ({
|
||||
stage,
|
||||
leads: enterprise.pipeline.filter(l => l.stage === stage),
|
||||
}));
|
||||
|
||||
const totalContractValue = enterprise.activeContracts.reduce((sum, c) => sum + c.pricePerMToken * c.tokensPerTick, 0);
|
||||
const totalSlaViolations = enterprise.activeContracts.reduce((sum, c) => sum + c.slaViolations, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 size={16} className="text-purple-400" />
|
||||
<span className="text-sm font-semibold">Enterprise Pipeline</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs text-surface-400">
|
||||
<span>Leads: <span className="font-mono text-surface-200">{enterprise.pipeline.length}</span></span>
|
||||
<span>Contracts: <span className="font-mono text-surface-200">{enterprise.activeContracts.length}</span></span>
|
||||
<span>Lead Rate: <span className="font-mono text-surface-200">{enterprise.leadGenerationRate.toFixed(3)}/t</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{leadsByStage.map(({ stage, leads }) => (
|
||||
<div key={stage} className="bg-surface-900 border border-surface-700 rounded-xl p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className={`w-2 h-2 rounded-full ${STAGE_COLORS[stage]}`} />
|
||||
<span className="text-xs font-semibold">{STAGE_LABELS[stage]}</span>
|
||||
<span className="text-xs text-surface-500 ml-auto">{leads.length}</span>
|
||||
</div>
|
||||
<div className="space-y-1.5 max-h-40 overflow-y-auto">
|
||||
{leads.length === 0 && (
|
||||
<div className="text-xs text-surface-600 italic">No leads</div>
|
||||
)}
|
||||
{leads.map(lead => (
|
||||
<div key={lead.id} className="bg-surface-800 rounded px-2 py-1.5 text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-surface-200 truncate">{lead.companyName}</span>
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${SEGMENT_BADGES[lead.segment].color}`}>
|
||||
{SEGMENT_BADGES[lead.segment].label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-surface-400 mt-0.5">
|
||||
<span>{formatMoney(lead.dealValue)}/yr</span>
|
||||
<span>Win: {formatPercent(lead.winProbability)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-semibold">Active Contracts</span>
|
||||
{totalSlaViolations > 0 && (
|
||||
<span className="flex items-center gap-1 text-xs text-warning">
|
||||
<AlertTriangle size={12} /> {totalSlaViolations} SLA violations
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{enterprise.activeContracts.length === 0 ? (
|
||||
<p className="text-xs text-surface-500">No active contracts. Build your sales team and model quality to attract enterprise customers.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-surface-400 border-b border-surface-700">
|
||||
<th className="text-left py-1.5 pr-3">Customer</th>
|
||||
<th className="text-left py-1.5 px-2">Segment</th>
|
||||
<th className="text-right py-1.5 px-2">Tokens/s</th>
|
||||
<th className="text-right py-1.5 px-2">$/M tok</th>
|
||||
<th className="text-right py-1.5 px-2">SLA</th>
|
||||
<th className="text-right py-1.5 px-2">Satisfaction</th>
|
||||
<th className="text-right py-1.5 px-2">Remaining</th>
|
||||
<th className="text-right py-1.5 pl-2">Renewal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{enterprise.activeContracts.map(c => {
|
||||
const remaining = Math.max(0, c.startTick + c.durationTicks - tickCount);
|
||||
const uptime = c.totalTicks > 0 ? c.uptimeTicks / c.totalTicks : 1;
|
||||
return (
|
||||
<tr key={c.id} className="border-b border-surface-800">
|
||||
<td className="py-1.5 pr-3 font-medium text-surface-200">{c.customerName}</td>
|
||||
<td className="py-1.5 px-2">
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${SEGMENT_BADGES[c.segment].color}`}>
|
||||
{SEGMENT_BADGES[c.segment].label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-right py-1.5 px-2 font-mono">{formatNumber(c.tokensPerTick)}</td>
|
||||
<td className="text-right py-1.5 px-2 font-mono text-success">{formatMoney(c.pricePerMToken)}</td>
|
||||
<td className="text-right py-1.5 px-2 font-mono">
|
||||
<span className={uptime >= c.slaUptime ? 'text-success' : 'text-danger'}>
|
||||
{formatPercent(uptime)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-right py-1.5 px-2 font-mono">{formatPercent(c.satisfaction)}</td>
|
||||
<td className="text-right py-1.5 px-2 font-mono">{formatNumber(remaining)}t</td>
|
||||
<td className="text-right py-1.5 pl-2 font-mono">{formatPercent(c.renewalProbability)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user