vividness pass: agent avatars + online pulse + getting-started empty state (0.27.0)

- generated gradient avatars w/ tag-based category emoji on agent cards
- pulsing online-status indicator
- actionable 3-step 'getting started' empty state on Browse (publish -> run -> use)
This commit is contained in:
artheru 2026-06-10 04:58:49 +08:00
parent 099fd7616f
commit c6127747ed
6 changed files with 37 additions and 6 deletions

View File

@ -0,0 +1 @@
{"sessionId":"17e654e8-7dd5-4856-85c5-0cfeb7ab736b","pid":3488121,"procStart":"252909119","acquiredAt":1781038486936}

View File

@ -7,7 +7,7 @@
name="description"
content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries."
/>
<meta name="version" content="0.26.0-mansion" />
<meta name="version" content="0.27.0-vivid" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

View File

@ -1,6 +1,6 @@
{
"name": "amerc-site",
"version": "0.26.0-mansion",
"version": "0.27.0-vivid",
"private": true,
"type": "module",
"scripts": {

View File

@ -2,6 +2,9 @@ import React, { useCallback, useEffect, useState } from 'react';
import { api } from './api.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 }) {
@ -38,16 +41,29 @@ export function AgentBrowse({ auth, setRoute }) {
<div className="ap-grid">
{classes.map((c) => (
<button key={c.id} className="ap-card" onClick={() => setOpen(c.id)}>
<div className="ap-card-top"><strong>{c.name}</strong><span className="ap-kind">{c.kind}</span></div>
<div className="ap-card-head2">
<span className="ap-avatar" style={avatarStyle(c.name)}>{classEmoji(c)}</span>
<div className="ap-card-titles"><strong>{c.name}</strong><span className="ap-kind">{c.kind}</span></div>
</div>
<p className="ap-desc">{c.description || 'No description.'}</p>
<div className="ap-card-tags">{c.tags.map((t) => <span key={t}>{t}</span>)}</div>
<div className="ap-card-foot">
<span>by {c.owner}</span>
<span className="ap-online"><i style={{ background: c.onlineCount ? STATUS_COLOR.online : '#555' }} />{c.onlineCount}/{c.instanceCount} online</span>
<span className="ap-online"><i className={c.onlineCount ? 'pulse' : ''} style={{ background: c.onlineCount ? STATUS_COLOR.online : '#555' }} />{c.onlineCount}/{c.instanceCount} online</span>
</div>
</button>
))}
{!classes.length && <p className="px-board-rows" style={{ opacity: 0.7 }}>No agent classes yet. Open <b>My Booth</b> to publish one.</p>}
{!classes.length && (
<div className="ap-empty-start">
<h3>No agents here yet be the first.</h3>
<ol>
<li><b>Publish a class</b> in <button className="ap-link" onClick={() => setRoute('booth')}>My Booth</button> name it, tag it (e.g. <code>backend</code>, <code>model</code>).</li>
<li><b>Run an instance</b> your broker connects and it goes <span style={{ color: STATUS_COLOR.online }}>online</span>.</li>
<li><b>Use it</b> embed it as a chatbox or drive it through a web terminal.</li>
</ol>
<button className="px-action" onClick={() => setRoute('booth')}>Open My Booth </button>
</div>
)}
</div>
{open && <ClassModal id={open} auth={auth} onClose={() => { setOpen(null); load(); }} />}
</div>

View File

@ -6,7 +6,7 @@ import Mansion from './Mansion.jsx';
import TavernGame from './TavernGame.jsx';
import { api } from './api.js';
const RELEASE = '0.26.0-mansion';
const RELEASE = '0.27.0-vivid';
const NAV = [
{ id: 'home', label: 'Tavern' },

View File

@ -1073,3 +1073,17 @@ button {
.mn-created p { margin: 0 0 8px; font-size: 13px; color: #d8c4a0; } .mn-created code { background: #07060c; padding: 1px 5px; border-radius: 4px; color: #7fe9ff; }
.mn-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid #241a36; font-size: 13px; }
.mn-row a { color: #27d8ff; } .mn-row span { color: #8a7aa8; margin-left: auto; }
/* agent card avatars + online pulse + onboarding (0.27 vividness) */
.ap-card-head2 { display: flex; align-items: center; gap: 11px; }
.ap-avatar { width: 40px; height: 40px; border-radius: 11px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex: none; box-shadow: 0 3px 10px rgba(0,0,0,0.45), inset 0 0 0 1px rgba(255,255,255,0.12); }
.ap-card-titles { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ap-card-titles strong { font-size: 15px; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ap-online i.pulse { box-shadow: 0 0 0 0 rgba(54,240,176,0.7); animation: ap-pulse 1.8s infinite; }
@keyframes ap-pulse { 0% { box-shadow: 0 0 0 0 rgba(54,240,176,0.6); } 70% { box-shadow: 0 0 0 6px rgba(54,240,176,0); } 100% { box-shadow: 0 0 0 0 rgba(54,240,176,0); } }
.ap-empty-start { grid-column: 1 / -1; max-width: 560px; margin: 10px auto; padding: 26px 28px; text-align: left; background: linear-gradient(160deg, #16102a, #0e0a18); border: 1px solid #2a1f3e; border-radius: 16px; box-shadow: 0 14px 40px rgba(0,0,0,0.5); }
.ap-empty-start h3 { margin: 0 0 14px; color: #fff; font-size: 18px; font-family: Georgia, serif; }
.ap-empty-start ol { margin: 0 0 18px; padding-left: 20px; display: flex; flex-direction: column; gap: 10px; }
.ap-empty-start li { color: #c9b8e6; font-size: 13px; line-height: 1.6; }
.ap-empty-start code { background: #07060c; color: #7fe9ff; padding: 1px 6px; border-radius: 4px; }
.ap-link { background: none; color: #27d8ff; cursor: pointer; text-decoration: underline; font: inherit; padding: 0; }