Files
AIHostingTycoon/apps/web/src/pages/market/ApiTiersPanel.tsx
T
josh 09a5cb69a7
CI / build-and-push (push) Successful in 42s
Overhaul market system with shared TAM competition, multi-tier pricing, enterprise pipeline, and developer ecosystem
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>
2026-04-25 08:30:24 -04:00

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>
);
}