diff --git a/apps/web/src/components/common/Tooltip.tsx b/apps/web/src/components/common/Tooltip.tsx new file mode 100644 index 0000000..1bfcb96 --- /dev/null +++ b/apps/web/src/components/common/Tooltip.tsx @@ -0,0 +1,42 @@ +import { useState, useRef, type ReactNode } from 'react'; + +interface TooltipProps { + content: ReactNode; + children: ReactNode; + position?: 'top' | 'bottom' | 'left' | 'right'; +} + +export function Tooltip({ content, children, position = 'bottom' }: TooltipProps) { + const [visible, setVisible] = useState(false); + const timeoutRef = useRef>(undefined); + + const show = () => { + clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setVisible(true), 300); + }; + + const hide = () => { + clearTimeout(timeoutRef.current); + setVisible(false); + }; + + const positionClasses = { + top: 'bottom-full left-1/2 -translate-x-1/2 mb-2', + bottom: 'top-full left-1/2 -translate-x-1/2 mt-2', + left: 'right-full top-1/2 -translate-y-1/2 mr-2', + right: 'left-full top-1/2 -translate-y-1/2 ml-2', + }; + + return ( +
+ {children} + {visible && ( +
+
+ {content} +
+
+ )} +
+ ); +} diff --git a/apps/web/src/components/layout/TopBar.tsx b/apps/web/src/components/layout/TopBar.tsx index a9ba0cb..3cc92c0 100644 --- a/apps/web/src/components/layout/TopBar.tsx +++ b/apps/web/src/components/layout/TopBar.tsx @@ -1,15 +1,19 @@ -import { Pause, Play, Bell, Zap } from 'lucide-react'; +import { type ReactNode } from 'react'; +import { Pause, Play, Bell } from 'lucide-react'; import { useGameStore } from '@/store'; -import { formatMoney, formatNumber, formatDuration } from '@ai-tycoon/shared'; +import { formatMoney, formatNumber, formatDuration, formatPercent } from '@ai-tycoon/shared'; import type { GameSpeed } from '@ai-tycoon/shared'; +import { Tooltip } from '@/components/common/Tooltip'; const SPEEDS: GameSpeed[] = [1, 2, 5]; export function TopBar() { const money = useGameStore((s) => s.economy.money); const revenuePerTick = useGameStore((s) => s.economy.revenuePerTick); + const expensesPerTick = useGameStore((s) => s.economy.expensesPerTick); const reputation = useGameStore((s) => s.reputation.score); const totalFlops = useGameStore((s) => s.infrastructure.totalFlops); + const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization); const isPaused = useGameStore((s) => s.meta.isPaused); const gameSpeed = useGameStore((s) => s.meta.gameSpeed); const tickCount = useGameStore((s) => s.meta.tickCount); @@ -18,14 +22,44 @@ export function TopBar() { const notifications = useGameStore((s) => s.notifications); const unreadCount = notifications.filter(n => !n.read).length; + const netIncome = revenuePerTick - expensesPerTick; + return (
- 0 ? 'up' : 'neutral'} /> - - - - + 0 ? 'up' : netIncome < 0 ? 'down' : 'neutral'} + tooltip={
+
Revenue: {formatMoney(revenuePerTick)}/s
+
Expenses: {formatMoney(expensesPerTick)}/s
+
Net: = 0 ? 'text-success' : 'text-danger'}>{formatMoney(netIncome)}/s
+
} + /> + + +
Total compute from all healthy GPUs
+
Utilization: {formatPercent(inferenceUtil)}
+
} + /> + +
@@ -65,9 +99,14 @@ export function TopBar() { ); } -function KPI({ label, value, trend }: { label: string; value: string; trend?: 'up' | 'down' | 'neutral' }) { - return ( -
+function KPI({ label, value, trend, tooltip }: { + label: string; + value: string; + trend?: 'up' | 'down' | 'neutral'; + tooltip?: ReactNode; +}) { + const content = ( +
{label}
); + + if (tooltip) { + return {content}; + } + return content; } diff --git a/apps/web/src/pages/ModelsPage.tsx b/apps/web/src/pages/ModelsPage.tsx index 5806d0b..82a52bb 100644 --- a/apps/web/src/pages/ModelsPage.tsx +++ b/apps/web/src/pages/ModelsPage.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Brain, Play, Rocket, Settings2 } from 'lucide-react'; +import { Brain, Play, Rocket } from 'lucide-react'; import { useGameStore } from '@/store'; import { formatNumber, formatPercent, formatDuration } from '@ai-tycoon/shared'; @@ -18,7 +18,7 @@ export function ModelsPage() { const trainingFlops = totalFlops * trainingAlloc; const estimatedTicks = trainingFlops > 0 ? Math.max(30, Math.ceil(120 / (1 + trainingFlops * 0.1))) : Infinity; - const estimatedCapability = Math.min(100, Math.log(1 + trainingFlops * 0.5) * 10 + Math.log(1 + totalData / 1e9) * 5); + const estimatedCapability = Math.min(95, Math.sqrt(trainingFlops) * 5 + Math.log10(1 + totalData / 1e8) * 10); const handleStartTraining = () => { if (activeTraining || trainingFlops === 0) return; @@ -135,18 +135,13 @@ export function ModelsPage() { {model.isDeployed ? ( Deployed ) : ( - <> - {productLines.filter(pl => pl.type === 'text-api' || pl.type === 'chat-product').map(pl => ( - - ))} - + )}
diff --git a/apps/web/src/pages/SettingsPage.tsx b/apps/web/src/pages/SettingsPage.tsx index ce2ed45..ecab786 100644 --- a/apps/web/src/pages/SettingsPage.tsx +++ b/apps/web/src/pages/SettingsPage.tsx @@ -1,8 +1,10 @@ +import { useRef } from 'react'; import { useGameStore } from '@/store'; export function SettingsPage() { const settings = useGameStore((s) => s.meta.settings); const companyName = useGameStore((s) => s.meta.companyName); + const fileInputRef = useRef(null); const handleReset = () => { if (confirm('Are you sure you want to reset all progress? This cannot be undone.')) { @@ -23,6 +25,31 @@ export function SettingsPage() { URL.revokeObjectURL(url); }; + const handleImport = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const data = JSON.parse(event.target?.result as string); + if (!data.meta?.companyName) { + alert('Invalid save file: missing company data.'); + return; + } + if (!confirm(`Import save for "${data.meta.companyName}"? This will replace your current game.`)) { + return; + } + localStorage.setItem('ai-tycoon-save', JSON.stringify({ state: data })); + window.location.reload(); + } catch { + alert('Failed to read save file. Make sure it is a valid AI Tycoon export.'); + } + }; + reader.readAsText(file); + if (fileInputRef.current) fileInputRef.current.value = ''; + }; + return (

Settings

@@ -56,6 +83,19 @@ export function SettingsPage() { > Export Save + +