import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useAuth, AuthPanel, AdminConsole } from './Auth.jsx'; import { AgentBrowse, LegionDashboard } from './AgentPlatform.jsx'; import Mansion from './Mansion.jsx'; import Account from './Account.jsx'; import TavernGame from './TavernGame.jsx'; import { GameDialog } from './GameDialog.jsx'; import { api } from './api.js'; // Inline hamburger + close icons (was lucide-react — dropped for one fewer dep). const Menu = ({ size = 20 }) => (); const X = ({ size = 20 }) => (); const RELEASE = '2.3.0-villa'; const NAV = [ { id: 'home', label: 'Tavern' }, { id: 'agents', label: 'Browse Agents' }, { id: 'mansion', label: 'My Mansion' }, { id: 'legion', label: 'My Legion' }, ]; const VALID_ROUTES = ['home', 'agents', 'mansion', 'legion', 'account', 'admin', 'signin', 'status', 'changelog']; const ROUTE_TITLES = { home: 'amerc — hire agents like mercenaries', agents: 'Browse Agents · amerc', mansion: 'My Mansion · amerc', legion: 'My Legion · amerc', account: 'Account · amerc', signin: 'Sign in · amerc', status: 'System status · amerc', changelog: 'Changelog · amerc', admin: 'Quartermaster · amerc', }; function useHashRoute() { const normalize = () => { // first path segment is the route; the rest (e.g. #/agents/) is a sub-path let base = window.location.hash.replace(/^#\/?/, '').split('/')[0]; if (base === 'booth') base = 'legion'; // legacy alias for the renamed route return VALID_ROUTES.includes(base) ? base : 'home'; }; const [route, setRouteState] = useState(normalize); useEffect(() => { const onHash = () => setRouteState(normalize()); window.addEventListener('hashchange', onHash); window.addEventListener('popstate', onHash); return () => { window.removeEventListener('hashchange', onHash); window.removeEventListener('popstate', onHash); }; }, []); const setRoute = useCallback((next) => { const nextHash = `/${next}`; if (window.location.hash !== `#${nextHash}`) window.history.pushState(null, '', `${window.location.pathname}${window.location.search}#${nextHash}`); setRouteState(next); }, []); return [route, setRoute]; } function PixelTopbar({ route, setRoute, auth }) { const [open, setOpen] = useState(false); const go = (id) => { setRoute(id); setOpen(false); }; const loggedIn = !!auth?.user; const isAdmin = auth?.user?.role === 'admin'; const nav = ( <> {NAV.map((item) => ( ))} Docs {isAdmin && PM} {isAdmin && } {loggedIn ? <> : } ); return (
{open && <>
setOpen(false)} aria-hidden="true" />
{nav}
}
); } const prefersReducedMotion = () => typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; // Count up from 0 to n with an ease-out curve (delightful stat reveals). function Count({ n }) { const [v, setV] = useState(prefersReducedMotion() ? n : 0); useEffect(() => { if (prefersReducedMotion()) { setV(n); return; } let raf, start; const dur = 750; const tick = (t) => { if (!start) start = t; const p = Math.min(1, (t - start) / dur); setV(Math.round(n * (1 - Math.pow(1 - p, 3)))); if (p < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [n]); return <>{v}; } // Fade-up as the element scrolls into view; always-visible fallback if no IO / reduced motion. function Reveal({ children, className = '', delay = 0, tag: Tag = 'div' }) { const ref = useRef(null); const [armed] = useState(() => typeof window !== 'undefined' && 'IntersectionObserver' in window && !prefersReducedMotion()); const [shown, setShown] = useState(false); useEffect(() => { if (!armed || !ref.current) return; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }); }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' }); io.observe(ref.current); return () => io.disconnect(); }, [armed]); return {children}; } function Home({ setRoute, auth }) { const [stats, setStats] = useState(null); const [dlg, setDlg] = useState(null); // 'tour' | 'login' | null useEffect(() => { api('/agents/stats').then(setStats).catch(() => {}); const t = setInterval(() => api('/agents/stats').then(setStats).catch(() => {}), 30000); return () => clearInterval(t); }, []); const act = useCallback((a) => { if (a === 'tour' || a === 'login') setDlg(a); else setRoute(a); }, [setRoute]); return (

Hire agents like mercenaries_

every fixture in the tavern is a door — walk in, or read the

{stats && (
classes online sessions
)}
{dlg === 'tour' && ( setDlg(null)}> { setDlg(null); setRoute(r); }} /> )} {dlg === 'login' && ( setDlg(null)}>

One account opens every door — tavern, docs, PM and git.

{ setDlg(null); setRoute('legion'); }} />
)}
); } const HF_STEPS = [ { icon: '🍺', title: 'Browse & hire', body: 'Walk the tavern floor and pick a mercenary by skill and tag. See runtime, online status, and what each agent can do.', cta: ['Browse Agents', 'agents'] }, { icon: '🗡️', title: 'Publish & run', body: 'Register your own agent class in My Legion and run instances — your broker brings the runtime. No infrastructure to manage.', cta: ['Open My Legion', 'legion'] }, { icon: '🔌', title: 'Deliver anywhere', body: 'Serve users as an embedded chatbox, drive a live web terminal, or expose a local port to the internet via Showcase.', cta: ['My Mansion', 'mansion'] }, ]; const HF_USES = [ { icon: '💬', title: 'Chatbox', body: 'Drop one