import React, { useCallback, useEffect, useState } from 'react';
import { api } from './api.js';
import { copyToast } from './toast.js';
const STATUS_COLOR = { online: '#36f0b0', pending: '#f2b85f', lost: '#ff9f5a', down: '#ff7a8c' };
const TAG_EMOJI = { backend: 'π οΈ', model: 'π§ ', frontend: 'π¨', ui: 'ποΈ', web: 'π', data: 'π', devops: 'βοΈ', ml: 'π€', security: 'π', api: 'π', bot: 'π€', chat: 'π¬', test: 'π§ͺ', docs: 'π', design: 'βοΈ', ops: 'π¦', research: 'π¬' };
const classEmoji = (c) => { for (const t of (c.tags || [])) if (TAG_EMOJI[t]) return TAG_EMOJI[t]; return 'π‘οΈ'; };
function avatarStyle(name) { let h = 0; for (const ch of String(name)) h = (h * 31 + ch.charCodeAt(0)) >>> 0; const a = h % 360, b = (a + 70 + h % 90) % 360; return { background: `linear-gradient(135deg, hsl(${a} 68% 46%), hsl(${b} 64% 32%))` }; }
// ---- Browse Agents: all public classes, filter by tag --------------------
export function AgentBrowse({ auth, setRoute }) {
const [classes, setClasses] = useState([]);
const [tags, setTags] = useState({});
const [tag, setTag] = useState('');
const [q, setQ] = useState('');
const [open, setOpen] = useState(null);
const [err, setErr] = useState('');
const [loaded, setLoaded] = useState(false);
const load = useCallback(async () => {
setErr('');
try {
const [c, s] = await Promise.all([api(`/agents/classes${tag ? `?tag=${encodeURIComponent(tag)}` : ''}`), api('/agents/stats')]);
setClasses((c.classes || []).filter((x) => !q || (x.name + x.tags.join() + x.description).toLowerCase().includes(q.toLowerCase())));
setTags(s.tags || {});
} catch (e) { setErr(e.message); } finally { setLoaded(true); }
}, [tag, q]);
useEffect(() => { load(); }, [load]);
return (
setTag('')}>β
all
{Object.entries(tags).sort((a, b) => b[1] - a[1]).map(([t, n]) => (
setTag(t)}>{TAG_EMOJI[t] || 'π‘οΈ'} {t} {n}
))}
{err &&
{err}
}
{!loaded && Array.from({ length: 3 }).map((_, i) => (
))}
{loaded && classes.map((c) => (
setOpen(c.id)}>
{classEmoji(c)}
{c.name} {c.kind}
{c.description || 'No description.'}
{c.tags.map((t) => {t} )}
by {c.owner}
{c.onlineCount}/{c.instanceCount} online
))}
{loaded && classes.length > 0 && (
setRoute('booth')}>
οΌ
Publish your own
Register an agent class and run it from your booth β your mercenary joins the roster right here.
Open My Booth β
)}
{loaded && !classes.length && (
No agents here yet β be the first.
Publish a class in setRoute('booth')}>My Booth β name it, tag it (e.g. backend, model).
Run an instance β your broker connects and it goes online .
Use it β embed it as a chatbox or drive it through a web terminal.
setRoute('booth')}>Open My Booth β
)}
{open &&
{ setOpen(null); load(); }} />}
);
}
function ClassModal({ id, auth, onClose }) {
const [data, setData] = useState(null);
const [sub, setSub] = useState(null);
const [use, setUse] = useState(null); // instance to use
const [err, setErr] = useState('');
const load = useCallback(() => api(`/agents/classes/${id}`).then(setData).catch((e) => setErr(e.message)), [id]);
useEffect(() => { load(); }, [load]);
const subscribe = async () => { try { const r = await api(`/agents/classes/${id}/subscribe`, { method: 'POST' }); setSub(r.token); } catch (e) { setErr(e.message); } };
if (!data) return null;
const c = data.class;
return (
e.stopPropagation()}>
{classEmoji(c)}
{c.name} {c.kind} by {c.owner}
β
{c.description}
{c.tags.map((t) => {t} )}
{err &&
{err}
}
{auth.user ? Get subscription token : Log in to subscribe. }
{sub && copyToast(sub)}>{sub} β§}
Instances
{data.instances.map((i) => (
#{i.id} {i.name}
{i.status}{i.broker ? ` Β· ${i.broker}` : ''} Β· {i.visibility}
setUse(i)}>Use βΈ
))}
{!data.instances.length &&
No instances published. The owner can publish one in My Booth.
}
{use &&
setUse(null)} />}
);
}
// ---- Use an instance: chatbox + web-pty ----------------------------------
export function UsePanel({ instance, onClose }) {
const [mode, setMode] = useState('chatbox');
const [sess, setSess] = useState(null);
const [accesskey, setAccesskey] = useState(instance.accesskey || '');
const [err, setErr] = useState('');
const start = useCallback(async () => {
setErr('');
try {
const r = await api(`/agents/instances/${instance.id}/sessions`, { method: 'POST', body: { connector: mode, accesskey: accesskey || undefined } });
setSess(r);
} catch (e) { setErr(e.message); }
}, [instance.id, mode, accesskey]);
useEffect(() => { if (instance.accesskey) start(); }, []); // owner: auto-start
return (
{ setMode('chatbox'); setSess(null); }}>Chatbox
{ setMode('webpty'); setSess(null); }}>Web PTY
close β
{!sess && (
{!instance.accesskey && instance.visibility !== 'public' && (
setAccesskey(e.target.value)} />
)}
Connect {mode}
{err &&
{err}
}
)}
{sess && (
<>
{mode === 'chatbox' && (
π Embed this agent on any website copyToast(``, 'Embed snippet copied')}>Copy snippet
{``}
Paste before </body> β a chat bubble appears bottom-right, and on connect your page's skills are injected into the agent.
)}
{mode === 'webpty' &&
π₯οΈ Switch to the εε° / Backstage tab above to watch and drive the agent's live terminal.
}
>
)}
);
}
// ---- My Booth: publish classes + instances, manage ------------------------
export function KeysCard() {
const [keys, setKeys] = useState([]);
const [name, setName] = useState('');
const [created, setCreated] = useState(null);
const [err, setErr] = useState('');
const load = useCallback(() => api('/keys').then((d) => setKeys(d.keys || [])).catch((e) => setErr(e.message)), []);
useEffect(() => { load(); }, [load]);
const mint = async (e) => { e.preventDefault(); setErr(''); try { const r = await api('/keys', { method: 'POST', body: { name } }); setCreated(r); setName(''); load(); } catch (e2) { setErr(e2.message); } };
const del = async (id) => { try { await api(`/keys/${id}`, { method: 'DELETE' }); load(); } catch (e2) { setErr(e2.message); } };
return (
π My agent keys
Mint a key for your own agent to read/write amerc docs, netdisk and the agent platform. Send it as Authorization: Bearer <key>.
{created &&
β
minted β copy now, shown once:
copyToast(created.key)}>{created.key} β§}
{err &&
{err}
}
{keys.map((k) => (
{k.prefix}β¦ {k.name}
{k.last_used ? `used ${new Date(k.last_used).toLocaleDateString()}` : 'never used'}
del(k.id)}>revoke
))}
{!keys.length &&
No keys yet β mint one above.
}
);
}
export function BoothDashboard({ auth, setRoute }) {
const [classes, setClasses] = useState([]);
const [insts, setInsts] = useState([]);
const [err, setErr] = useState('');
const [form, setForm] = useState({ name: '', tags: '', description: '', kind: 'codex', command: 'codex --yolo', visibility: 'public' });
const [newInst, setNewInst] = useState(null);
const load = useCallback(async () => {
setErr('');
try {
const c = await api('/agents/classes?mine=1');
setClasses(c.classes || []);
const all = await Promise.all((c.classes || []).map((x) => api(`/agents/instances?classId=${x.id}`).then((r) => r.instances).catch(() => [])));
setInsts(all.flat());
} catch (e) { setErr(e.message); }
}, []);
useEffect(() => { if (auth.user) load(); }, [auth.user, load]);
if (!auth.ready) return ;
if (!auth.user) return (
My Booth Log in to publish and run agents.
setRoute('signin')}>Go to Login
);
const publishClass = async (e) => {
e.preventDefault();
try { await api('/agents/classes', { method: 'POST', body: { ...form, tags: form.tags.split(',').map((t) => t.trim()).filter(Boolean) } }); setForm({ ...form, name: '', tags: '', description: '' }); load(); }
catch (e2) { setErr(e2.message); }
};
const publishInstance = async (classId) => {
try { const r = await api('/agents/instances', { method: 'POST', body: { classId, visibility: 'public' } }); setNewInst(r); load(); }
catch (e2) { setErr(e2.message); }
};
const shutdown = async (i) => { try { await api(`/agents/instances/${i.id}`, { method: 'DELETE', body: { accesskey: i.accesskey } }); load(); } catch (e2) { setErr(e2.message); } };
return (
My Booth {auth.user.handle}
{err &&
{err}
}
{!classes.length ? (
π‘οΈ
Welcome to your booth, {auth.user.handle}.
This is where you arm and dispatch your mercenaries. Three steps to go live:
Publish a class β name your agent and tag its skills, in the form on the left.
Run an instance β your broker connects and it turns online .
Deliver it β embed a chatbox, drive a web terminal, or expose a local port via the Mansion .
Your broker authenticates with an instance accesskey (shown when you publish one) or an agent key β mint one below.
) : (
<>
My classes
{classes.map((c) => (
{classEmoji(c)}
{c.name} {c.visibility}
{c.onlineCount}/{c.instanceCount}
publishInstance(c.id)}>+ Publish instance
{c.tags.map((t) => {t} )}
))}
My instances
{insts.map((i) => (
#{i.id} {i.status}
{i.accesskey && copyToast(i.accesskey)}>{i.accesskey.slice(0, 14)}β¦ β§}
shutdown(i)}>shut down
))}
{!insts.length &&
No instances. Publish one from a class above.
}
>
)}
{newInst && (() => {
const hb = `while true; do curl -s -XPOST https://amerc.ai/api/agents/instances/${newInst.id}/heartbeat -H 'content-type: application/json' -d '{"accesskey":"${newInst.accesskey}","status":"online","broker":"my-broker"}' >/dev/null; sleep 10; done`;
return (
setNewInst(null)}>
e.stopPropagation()}>
Instance #{newInst.id} β bring it online setNewInst(null)}>β
Registered, but offline until a broker checks in. Two steps bring it to life:
Heartbeat β keeps it marked online (check in every <30s). Paste this anywhere with curl:
{hb} copyToast(hb, 'Heartbeat loop copied')}>copy
Relay β bridge your agent so live sessions reach it, over the broker WebSocket:
copyToast(newInst.relayWs)}>{newInst.relayWs} β§
The amerc webagent broker speaks this relay for you β point it at the accesskey below.
accesskey Β· your broker's secret copyToast(newInst.accesskey)}>{newInst.accesskey} β§
);
})()}
);
}