swarm-ui: show each agent's icon on its card
Each agent card now leads with the agent's icon, loaded as an `<img>` from `GET /api/agents/<name>/icon`: the same 5em square, background and fallback as the hive dashboard's container row. An agent with no icon (the route's 404), or any other failed load, shows the dimmed hyperhive mark (`/favicon.svg`) instead of a broken image. Only ever an `<img>`, never inline markup: the body is an agent-authored SVG, and an image load does not run its script.
This commit is contained in:
parent
e974194e3a
commit
3b8df27dcf
3 changed files with 111 additions and 39 deletions
|
|
@ -465,7 +465,8 @@ credential publishes it: the queue grants that credential
|
||||||
none of the bucket. The harness writes once per start, because a config change
|
none of the bucket. The harness writes once per start, because a config change
|
||||||
reaches an agent by restarting its container. An agent with no icon deletes its
|
reaches an agent by restarting its container. An agent with no icon deletes its
|
||||||
key. Swarm-side, `GET /api/agents/<name>/icon` serves the stored bytes, and 404
|
key. Swarm-side, `GET /api/agents/<name>/icon` serves the stored bytes, and 404
|
||||||
means the agent has no icon.
|
means the agent has no icon. swarm-ui's agent cards load it as an `<img>` and show the
|
||||||
|
dimmed hyperhive mark for an agent without one, as the hive dashboard does.
|
||||||
|
|
||||||
### Swarm-wide forge objects
|
### Swarm-wide forge objects
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,37 @@
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
/* Icon beside the name/message, sized and dimmed like the hive
|
||||||
|
dashboard's `.container-icon` (dashboard.css). */
|
||||||
|
.ui-agent-card-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.7em;
|
||||||
|
}
|
||||||
|
.ui-agent-card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35em;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.ui-agent-card-icon {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: none;
|
||||||
|
width: 5em;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: color-mix(in srgb, var(--crust) 60%, transparent);
|
||||||
|
}
|
||||||
|
.ui-agent-card-icon > img {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.ui-agent-card-icon-none {
|
||||||
|
filter: grayscale(1);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
// `WantedMenu` below keeps its own clicks (mouse or keyboard-synthesized,
|
// `WantedMenu` below keeps its own clicks (mouse or keyboard-synthesized,
|
||||||
// `WantedMenu` hosts real `<button>`s) from also firing `Card`'s
|
// `WantedMenu` hosts real `<button>`s) from also firing `Card`'s
|
||||||
// `onClick` and opening the detail panel.
|
// `onClick` and opening the detail panel.
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
import { Badge } from "@hive/shared/badge.js";
|
import { Badge } from "@hive/shared/badge.js";
|
||||||
import type { ProblemDetails } from "@hive/shared/api-error.js";
|
import type { ProblemDetails } from "@hive/shared/api-error.js";
|
||||||
import { Card } from "../../ui/card/Card.js";
|
import { Card } from "../../ui/card/Card.js";
|
||||||
|
|
@ -46,6 +47,9 @@ export function AgentCard({
|
||||||
const { tone, label } = FRESHNESS[row.freshness];
|
const { tone, label } = FRESHNESS[row.freshness];
|
||||||
return (
|
return (
|
||||||
<Card selected={selected} onClick={() => onOpenDetail(row)}>
|
<Card selected={selected} onClick={() => onOpenDetail(row)}>
|
||||||
|
<div class="ui-agent-card-row">
|
||||||
|
<AgentIcon name={row.name} />
|
||||||
|
<div class="ui-agent-card-body">
|
||||||
<div class="ui-agent-card-line1">
|
<div class="ui-agent-card-line1">
|
||||||
<span class="ui-agent-card-name">{row.name}</span>
|
<span class="ui-agent-card-name">{row.name}</span>
|
||||||
<Badge
|
<Badge
|
||||||
|
|
@ -67,7 +71,10 @@ export function AgentCard({
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span class="ui-agent-card-wanted" onClick={(e) => e.stopPropagation()}>
|
<span
|
||||||
|
class="ui-agent-card-wanted"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<WantedMenu
|
<WantedMenu
|
||||||
row={row}
|
row={row}
|
||||||
pending={pending}
|
pending={pending}
|
||||||
|
|
@ -88,6 +95,36 @@ export function AgentCard({
|
||||||
<div class="ui-agent-card-message">
|
<div class="ui-agent-card-message">
|
||||||
{row.snapshot?.status_text ?? "—"}
|
{row.snapshot?.status_text ?? "—"}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The agent's icon from swarm-controller's `GET /api/agents/{name}/icon`,
|
||||||
|
// the same square and fallback as the hive dashboard's container row: an
|
||||||
|
// agent with no icon (404) or an unreachable controller shows the dimmed
|
||||||
|
// hyperhive mark instead of a broken image. Always an `<img>`: the body is
|
||||||
|
// an agent-authored SVG, and only an image load keeps its script from running.
|
||||||
|
function AgentIcon({ name }: { name: string }) {
|
||||||
|
const [fallback, setFallback] = useState(false);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
fallback
|
||||||
|
? "ui-agent-card-icon ui-agent-card-icon-none"
|
||||||
|
: "ui-agent-card-icon"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src={
|
||||||
|
fallback
|
||||||
|
? "/favicon.svg"
|
||||||
|
: `/api/agents/${encodeURIComponent(name)}/icon`
|
||||||
|
}
|
||||||
|
onError={() => setFallback(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue