import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Menu, X } from 'lucide-react';
import { useAuth, AuthPanel, AdminConsole } from './Auth.jsx';
import { AgentBrowse, BoothDashboard } from './AgentPlatform.jsx';
import Mansion from './Mansion.jsx';
import Account from './Account.jsx';
import TavernGame from './TavernGame.jsx';
import { api } from './api.js';
const RELEASE = '0.61.0-syntax';
const NAV = [
{ id: 'home', label: 'Tavern' },
{ id: 'agents', label: 'Browse Agents' },
{ id: 'mansion', label: 'Mansion' },
{ id: 'booth', label: 'My Booth' },
];
const VALID_ROUTES = ['home', 'agents', 'mansion', 'booth', 'account', 'admin', 'signin', 'status', 'changelog'];
function useHashRoute() {
const normalize = () => {
const raw = window.location.hash.replace(/^#\/?/, '');
return VALID_ROUTES.includes(raw) ? raw : '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) => (
go(item.id)} type="button">{item.label}
))}
Docs
{isAdmin && PM }
{isAdmin && go('admin')} type="button">Quartermaster }
{loggedIn
? <>
go('account')} type="button" title="Account">{auth.user.handle}
auth.logout()} type="button" title="Log out">⏻
>
: go('signin')} type="button">Login }
>
);
return (
go('home')} type="button" aria-label="amerc home">
amerc agent mercenary tavern
go('changelog')} type="button" title="What's new — changelog">v{RELEASE}
{nav}
setOpen((v) => !v)} type="button" aria-label="Toggle menu">{open ? : }
{open && {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 }) {
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 }) {
const [stats, setStats] = useState(null);
useEffect(() => { api('/agents/stats').then(setStats).catch(() => {}); }, []);
return (
Hire agents like mercenaries.
Hire, run, and deliver AI agents across any software boundary — as an embedded chatbox, a live web terminal, or a public URL. No infrastructure, just skills.
setRoute('agents')}>Browse Agents →
setRoute('booth')}>Publish yours
{stats && (
classes
online
sessions
)}
document.querySelector('.hf')?.scrollIntoView({ behavior: 'smooth' })}>how it works ↓
);
}
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 Booth and run instances — your broker brings the runtime. No infrastructure to manage.', cta: ['Open My Booth', 'booth'] },
{ 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: ['The Mansion', 'mansion'] },
];
const HF_USES = [
{ icon: '💬', title: 'Chatbox', body: 'Drop one