f9f6233b69
CI / build-and-push (push) Successful in 33s
Addresses broken interactions (notification bell, browser dialogs), missing feedback states (disabled buttons, pricing changes, paused indicator), unclear affordances (research queue, model tuning, funding requirements), and navigation gaps (hash routing, keyboard shortcuts, clickable dashboard cards, sidebar grouping, tutorial hints). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
7.5 KiB
TypeScript
189 lines
7.5 KiB
TypeScript
import { useState } from 'react';
|
|
import { Users, Plus, Star, Briefcase } from 'lucide-react';
|
|
import { useGameStore } from '@/store';
|
|
import { formatMoney } from '@ai-tycoon/shared';
|
|
import { KEY_HIRE_POOL } from '@ai-tycoon/game-engine';
|
|
import type { DepartmentId } from '@ai-tycoon/shared';
|
|
|
|
const DEPT_LABELS: Record<string, string> = {
|
|
research: 'Research',
|
|
engineering: 'Engineering',
|
|
operations: 'Operations',
|
|
sales: 'Sales',
|
|
};
|
|
|
|
const DEPT_DESCRIPTIONS: Record<string, string> = {
|
|
research: 'Improves R&D speed and model quality',
|
|
engineering: 'Faster training and better infrastructure',
|
|
operations: 'Lower costs and higher uptime',
|
|
sales: 'More enterprise contracts and revenue',
|
|
};
|
|
|
|
export function TalentPage() {
|
|
const departments = useGameStore((s) => s.talent.departments);
|
|
const keyHires = useGameStore((s) => s.talent.keyHires);
|
|
const totalSalary = useGameStore((s) => s.talent.totalSalaryPerTick);
|
|
const money = useGameStore((s) => s.economy.money);
|
|
const era = useGameStore((s) => s.meta.currentEra);
|
|
const hireDepartment = useGameStore((s) => s.hireDepartment);
|
|
|
|
const [showKeyHires, setShowKeyHires] = useState(false);
|
|
|
|
const hiringCost = 2000;
|
|
const canHire = money >= hiringCost;
|
|
|
|
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
|
|
const currentEraIdx = eraOrder.indexOf(era);
|
|
const availableKeyHires = KEY_HIRE_POOL.filter(h => {
|
|
const hireEraIdx = eraOrder.indexOf(h.requiredEra);
|
|
if (hireEraIdx > currentEraIdx) return false;
|
|
return !keyHires.some(kh => kh.id === h.id);
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-2xl font-bold">Talent</h2>
|
|
<div className="text-sm text-surface-400">
|
|
Total Salary: <span className="text-danger font-mono">{formatMoney(totalSalary)}/s</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{Object.entries(departments).map(([id, dept]) => (
|
|
<div key={id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div>
|
|
<h3 className="font-semibold">{DEPT_LABELS[id]}</h3>
|
|
<p className="text-xs text-surface-400">{DEPT_DESCRIPTIONS[id]}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-2xl font-bold font-mono">{dept.headcount}</div>
|
|
<div className="text-xs text-surface-500">employees</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2 mb-3">
|
|
<StatBar label="Effectiveness" value={dept.effectiveness} />
|
|
<StatBar label="Morale" value={dept.morale} />
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs text-surface-400">
|
|
Budget: {formatMoney(dept.budget)}/mo
|
|
</span>
|
|
<div className="text-right">
|
|
<button
|
|
onClick={() => hireDepartment(id, 1)}
|
|
disabled={!canHire}
|
|
className="flex items-center gap-1 bg-surface-800 hover:bg-surface-700 border border-surface-600 rounded px-3 py-1.5 text-xs disabled:opacity-40 disabled:cursor-not-allowed"
|
|
>
|
|
<Plus size={12} />
|
|
Hire ({formatMoney(hiringCost)})
|
|
</button>
|
|
{!canHire && <div className="text-[10px] text-warning mt-0.5">Insufficient funds</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">
|
|
<h3 className="font-semibold flex items-center gap-2">
|
|
<Star size={16} className="text-yellow-400" />
|
|
Key Hires
|
|
</h3>
|
|
<button
|
|
onClick={() => setShowKeyHires(!showKeyHires)}
|
|
className="text-xs text-accent hover:text-accent-light"
|
|
>
|
|
{showKeyHires ? 'Hide Available' : `Show Available (${availableKeyHires.length})`}
|
|
</button>
|
|
</div>
|
|
|
|
{keyHires.length > 0 && (
|
|
<div className="space-y-2 mb-4">
|
|
{keyHires.map(hire => (
|
|
<div key={hire.id} className="flex items-center justify-between bg-surface-800 rounded-lg p-3">
|
|
<div>
|
|
<div className="text-sm font-medium">{hire.name}</div>
|
|
<div className="text-xs text-surface-400">
|
|
{DEPT_LABELS[hire.department]} · {hire.specialAbility}
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-surface-400">
|
|
{formatMoney(hire.salary)}/s
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{showKeyHires && (
|
|
<div className="space-y-2">
|
|
{availableKeyHires.length > 0 ? availableKeyHires.map(hire => (
|
|
<div key={hire.id} className="flex items-center justify-between bg-surface-800/50 border border-surface-700 rounded-lg p-3">
|
|
<div>
|
|
<div className="text-sm font-medium">{hire.name}</div>
|
|
<div className="text-xs text-surface-400">{hire.description}</div>
|
|
<div className="text-xs text-surface-500">
|
|
{DEPT_LABELS[hire.department]} · {formatMoney(hire.salary)}/s
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
const store = useGameStore.getState();
|
|
const keyHireObj = {
|
|
id: hire.id,
|
|
name: hire.name,
|
|
department: hire.department as DepartmentId,
|
|
specialAbility: hire.description,
|
|
effects: hire.effects,
|
|
salary: hire.salary,
|
|
hiredAtTick: store.meta.tickCount,
|
|
loyalty: hire.loyalty,
|
|
};
|
|
store.updateState({
|
|
talent: {
|
|
...store.talent,
|
|
keyHires: [...store.talent.keyHires, keyHireObj],
|
|
},
|
|
});
|
|
}}
|
|
disabled={money < 5000}
|
|
className="flex items-center gap-1 bg-accent hover:bg-accent-dark text-white rounded px-3 py-1.5 text-xs disabled:opacity-40"
|
|
>
|
|
<Briefcase size={12} />
|
|
Recruit ($5K)
|
|
</button>
|
|
</div>
|
|
)) : (
|
|
<p className="text-sm text-surface-500 text-center py-2">No key hires available in current era</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{keyHires.length === 0 && !showKeyHires && (
|
|
<p className="text-sm text-surface-500">No key hires recruited yet.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StatBar({ label, value }: { label: string; value: number }) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-surface-500 w-24">{label}</span>
|
|
<div className="flex-1 h-1.5 bg-surface-800 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full ${value > 0.7 ? 'bg-success' : value > 0.4 ? 'bg-warning' : 'bg-danger'}`}
|
|
style={{ width: `${value * 100}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-xs text-surface-400 font-mono w-8 text-right">{Math.round(value * 100)}%</span>
|
|
</div>
|
|
);
|
|
}
|