09a5cb69a7
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>
131 lines
6.1 KiB
TypeScript
131 lines
6.1 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { useGameStore } from '@/store';
|
|
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
|
|
import type { ApiTierId } from '@ai-tycoon/shared';
|
|
import { Code, Check } from 'lucide-react';
|
|
|
|
const TIER_ORDER: ApiTierId[] = ['free', 'payg', 'scale', 'enterprise-api'];
|
|
const TIER_COLORS: Record<ApiTierId, string> = {
|
|
free: 'border-surface-500',
|
|
payg: 'border-green-500',
|
|
scale: 'border-blue-500',
|
|
'enterprise-api': 'border-purple-500',
|
|
};
|
|
|
|
function useAppliedFeedback() {
|
|
const [show, setShow] = useState(false);
|
|
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
const trigger = useCallback(() => {
|
|
setShow(true);
|
|
clearTimeout(timerRef.current);
|
|
timerRef.current = setTimeout(() => setShow(false), 1200);
|
|
}, []);
|
|
useEffect(() => () => clearTimeout(timerRef.current), []);
|
|
return { show, trigger };
|
|
}
|
|
|
|
export function ApiTiersPanel() {
|
|
const apiTiers = useGameStore((s) => s.market.apiTiers);
|
|
const setApiTierPrice = useGameStore((s) => s.setApiTierPrice);
|
|
const toggleApiTier = useGameStore((s) => s.toggleApiTier);
|
|
const feedback = useAppliedFeedback();
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Code size={16} className="text-blue-400" />
|
|
<span className="text-sm font-semibold">API Tiers</span>
|
|
</div>
|
|
<div className="flex items-center gap-4 text-xs text-surface-400">
|
|
<span>Developers: <span className="font-mono text-surface-200">{formatNumber(apiTiers.totalDevelopers)}</span></span>
|
|
<span>Tokens/s: <span className="font-mono text-surface-200">{formatNumber(apiTiers.totalTokensPerTick)}</span></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-4 gap-3">
|
|
{TIER_ORDER.map(tierId => {
|
|
const tier = apiTiers.tiers[tierId];
|
|
return (
|
|
<div key={tierId} className={`bg-surface-900 border-t-2 ${TIER_COLORS[tierId]} border border-surface-700 rounded-xl p-4 space-y-3`}>
|
|
<div className="flex items-center justify-between">
|
|
<h4 className="font-semibold text-sm">{tier.config.name}</h4>
|
|
{tierId !== 'free' && (
|
|
<button
|
|
onClick={() => { toggleApiTier(tierId); feedback.trigger(); }}
|
|
className={`text-[10px] px-2 py-0.5 rounded-full ${tier.config.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
|
|
>
|
|
{tier.config.isActive ? 'Active' : 'Inactive'}
|
|
</button>
|
|
)}
|
|
{tierId === 'free' && (
|
|
<span className="text-[10px] px-2 py-0.5 rounded-full bg-surface-700 text-surface-400">Always On</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-2xl font-bold font-mono">{formatNumber(tier.developerCount)}</div>
|
|
<div className="text-xs text-surface-400">developers</div>
|
|
|
|
{tierId !== 'free' ? (
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label className="block text-[10px] text-surface-400 mb-1">
|
|
Monthly Fee ($)
|
|
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={tier.config.monthlyFee}
|
|
onChange={(e) => { setApiTierPrice(tierId, 'monthlyFee', Number(e.target.value)); feedback.trigger(); }}
|
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
|
min={0}
|
|
step={50}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<label className="block text-[10px] text-surface-400 mb-1">In $/M</label>
|
|
<input
|
|
type="number"
|
|
value={tier.config.inputTokenPrice}
|
|
onChange={(e) => { setApiTierPrice(tierId, 'inputTokenPrice', Number(e.target.value)); feedback.trigger(); }}
|
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
|
min={0}
|
|
step={0.1}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-[10px] text-surface-400 mb-1">Out $/M</label>
|
|
<input
|
|
type="number"
|
|
value={tier.config.outputTokenPrice}
|
|
onChange={(e) => { setApiTierPrice(tierId, 'outputTokenPrice', Number(e.target.value)); feedback.trigger(); }}
|
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
|
min={0}
|
|
step={0.1}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-xs text-surface-500">Free tier attracts developers to your ecosystem</div>
|
|
)}
|
|
|
|
<div className="text-xs space-y-1 text-surface-400">
|
|
<div className="flex justify-between">
|
|
<span>Rate Limit</span>
|
|
<span className="font-mono">{formatNumber(tier.config.rateLimit)} req/min</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span>Tokens/s</span>
|
|
<span className="font-mono">{formatNumber(tier.tokensPerTick)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|