Agent Dashboard
An agent-ops overview — KPI tiles whose trends are derived, a runs-over-time bar chart, a recent-runs table whose failing rows expand to the error line, and a live-agent rail that filters the table without moving the window figures.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/agent-dashboard.jsonPrompt
Build a React + TypeScript + Tailwind "AgentDashboard" block with zod and
lucide-react. It is the operations overview for a FLEET of agents over one time
window: a KPI strip, a runs-over-time bar chart, a recent-runs table and a rail
of live agents.
Contract
- One zod schema (a sibling contract file) is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready";
windowLabel: string; currency?: string;
stats: { runs, successRate, avgDurationMs, cost } — each of them
{ value: number; previous?: number };
series: { start: string|number|Date; runs: int; failed: int }[];
runs: { id, agentId?, agent, task, outcome, startedAt, durationMs,
cost?, error?: { message, code?, step? } }[];
agents: { id, name, state, task?, runs? }[] }.
- THREE SCOPES, never blurred. `stats` and `series` are window-scope facts about
every run in the window; `runs` is a SAMPLE (the most recent handful) and will
NOT sum to the stats; `agents` is now-scope — who is awake this second. The
block states the sample relationship in words ("the table samples the 12 most
recent of 139 runs in this window; the tiles above cover all of them") instead
of letting a reader discover it mid-incident.
- Outcomes are FIVE, not two: running | succeeded | partial | failed |
cancelled. `partial` is not a failure (painting it red trains people to ignore
red), `cancelled` is a human decision, and `running` exists because a dashboard
always has rows that have not finished — their duration cell says "so far".
- Deltas are DERIVED from value/previous, never fed in. A pre-computed "+12%" is
a number that can contradict the two numbers beside it. No `previous` ⇒ no
trend line at all, which is the honest rendering of "nothing to compare".
- `failed` is a SUBSET of a bucket's `runs`; `cost: undefined` means "not
reported" and must never render as 0; `error: undefined` on a failed run means
the failure went unreported.
- Props: the contract fields plus heading?, label?, tiles? (which KPIs and in
what order), maxRuns? (6), showChart? (true), showAgents? (true), copyable?
(true), locale? ("en-US"), timeZone? ("UTC"), formatDuration?, onSelectRun?,
onSelectAgent?, onRefresh?, onRetry?, errorMessage?, emptyState?, className and
the rest of the element props; ref forwarded to the root <section>.
Behavior
- THE BLOCK OWNS NO CLOCK. Every instant it prints comes from the payload; it
starts no interval and never derives a state from elapsed time, so a server
render, a client render and a screenshot of the same record agree.
- Four first-class envelope branches. `loading` is a skeleton in the READY shape
(four tiles, a chart band, table rows, a rail) with aria-busy and one polite
sr-only line; `empty` is "no agent has ever run in this workspace"; `error` is
role=alert with the raw message on its own line plus a retry you own; `ready`
is the block.
- QUIET IS NOT EMPTY. A window in which nothing ran is `ready` with zeros: the
chart draws its baselines, the caption says "no runs in this window", and the
success-rate tile prints an em dash — a rate over zero runs is undefined, and
"0% success" is the fastest way to start a false incident.
- KPI trends: each tile compares value to previous and knows its own GOOD
DIRECTION — success rate up is good, average duration and cost down are good,
and run count is NEUTRAL (more runs is neither, and colouring it would be an
editorial opinion the data never expressed). Rates are compared in percentage
POINTS ("-2.3 pts"), everything else relatively ("+12%"), a zero baseline is
handled in words ("up from zero"), and a sub-0.5% change reads "flat".
- Chart: buckets are sorted by start, the bucket width is inferred from the
MEDIAN gap (so one missing bucket cannot redefine the axis) and printed as
prose — "runs per hour", "runs per day". Each bar stacks the failed share on
top of the rest of the same bar, with `failed` clamped into `runs` so no bar
can draw taller than its own count. A bucket with any runs never falls below a
minimum height; a bucket with zero runs still draws a hairline so the reader
can see it exists. A dashed mean line crosses the plot. ONE derived model feeds
the picture, the caption and the svg's aria-label, so they cannot drift apart.
- The rail is a FILTER, not a roster. Clicking an agent narrows the table
(aria-pressed, click again or press Esc to clear) and fires onSelectAgent so a
host can mirror it into the URL — while the tiles and the chart above stay
window-scope, and an inline note says exactly that, plus how many runs that
agent had in the window versus how many are in this sample. Filtering the
sample must never silently rewrite figures that describe the whole window.
- The filter is CLAMPED, not reset: if the selected agent drops off the rail it
simply stops filtering, instead of leaving the table mysteriously empty.
- A second filter, "Problems only (N)", keeps failed and partial rows. It is
rendered only when N > 0 — a toggle that can only ever empty the table is
noise — and stops filtering when it disappears. Both filters compose, and one
polite live region announces the resulting count on TRANSITION only (never on
the first paint, which would talk over the reader on arrival).
- Expand-to-error: a row with an `error` payload gets a chevron whose
aria-expanded/aria-controls point at a detail row that is always rendered and
merely `hidden` while closed — so aria-controls never dangles. The detail shows
the code chip, the step, the started-at instant and the raw multi-line message.
A row with NO error payload gets no chevron at all; a failed row without one
says "no error reported" rather than offering a disclosure onto nothing.
- Truncation is announced: `maxRuns` rows are drawn above an explicit
"Show all 12 runs" toggle, and the sample sentence stays on screen. There is no
silent cap.
- onSelectRun is what turns task cells into buttons. Without it they are plain
text — no cursor, no hover, no dead affordance.
- Copy writes a plain-text digest: the window label, every KPI with its derived
delta, the chart caption, and every row the CURRENT FILTERS select (never the
truncated slice — what lands in an incident channel must be the whole filtered
view). The clipboard result is visible in all three outcomes (idle / copied /
failed) because a silent no-op on a blocked clipboard reads as a broken button;
a one-shot lock in a REF (not in state — the second click of a double-click
lands while the write is still in flight and reads a stale "idle") ignores
clicks from the moment one is issued until the confirmation clears, and the
reset timer lives in a ref, cleared before rescheduling and on unmount.
- Identity changes reset the view (adjust state during render, no effect): a new
run-id signature clears every expanded row, the reveal and the copy state, so a
recycled block never shows the previous payload's open error.
- Costs keep four decimals below a cent and two above it, anything under 0.0001
reads "< $0.0001", and a missing cost is an em dash. An unknown locale, IANA
zone or currency code falls back instead of throwing — a dashboard must not
take the page down over a typo in a prop. An unparseable instant renders as a
dash, never a RangeError.
Rendering & styling
- Semantic tokens only: bg-card / border / text-muted-foreground for chrome, and
two small tone tables — outcome → theme variable (running: var(--chart-1),
succeeded: var(--chart-2), partial: var(--chart-4), failed: var(--destructive),
cancelled: var(--muted-foreground)) and agent state → the same palette. Bars
are var(--chart-2) with var(--destructive) failures; badges tint themselves
with color-mix(in oklab, <tone> 10%, transparent). No hard-coded colours.
- Every outcome is a WORD plus a SHAPE (circle-check / triangle / octagon / ban /
spinner), so the block survives a monochrome theme and a colour-blind reader.
- The KPI strip is a <dl> in a repeat(auto-fit, minmax(min(11rem,100%),1fr)) grid
with a 1px gap over a bg-border parent: the gap draws the dividers, so
hairlines stay right at every wrap point without a border that dangles at the
end of a row. Figures are tabular-nums.
- The chart is inline SVG with preserveAspectRatio="none" and a viewBox of
slot-width units, so it fills any column; the mean line uses
vector-effect="non-scaling-stroke" to stay 1px through the distortion, and no
text lives inside the svg (it would stretch) — labels sit in the figcaption.
role="img" plus a sentence-long aria-label is the text alternative; each bucket
group carries a native <title> for pointer readout.
- Long task strings, shard ids and URLs use `wrap-anywhere` with `min-w-0`, NOT
`break-words`: only overflow-wrap:anywhere lowers the min-content width, which
is what actually stops a 60-character identifier from widening the table.
- The body is one flex row that stacks below lg: main column (chart + table) then
the rail, which becomes a full-width section under the table on narrow screens.
- Motion is decorative only — a rotating chevron, a hover tint on bar groups, a
pulse on thinking agents and a spinner on running rows — all
motion-reduce-guarded; the skeleton's pulse stops too, and every function still
works with animation off.
- cn() merges the consumer's className into the root, which spreads the remaining
element props, forwards its ref and composes (never replaces) their onKeyDown.
Customization levers
- Which tiles: `tiles` picks the KPI strip's contents AND their order — a sidebar
can ask for ["successRate","cost"] only. Unknown keys are ignored, an empty
list falls back to all four.
- Which blocks: `showChart={false}` gives a text-only ops strip; `showAgents={false}`
hands the full width to the table. Both are independent of the data.
- Sample budget: `maxRuns` decides how many rows are drawn before the reveal;
raise it for a full-page dashboard, lower it for a card, but never make it
silent — the count is part of the reading.
- Trend policy: the good-direction table is one object literal. A team that bills
by usage and WANTS more runs to read green changes one entry; a team with a
real "warning" token repoints `partial` there.
- Money and time: `currency` + `locale` swap number formatting wholesale,
`timeZone` pins the zone, `formatDuration` swaps the duration wording (a
compact "4m51s", a bare seconds count).
- Chrome: `heading` / `label` name the region, `copyable` hides the digest
button, `emptyState` replaces the empty body, `errorMessage` + `onRetry` own
the envelope failure, `onRefresh` adds a header reload, `onSelectRun` /
`onSelectAgent` wire the block into your routing.
- Density: the KPI grid's 11rem track and the rail's lg:w-64 are the two numbers
that decide the layout's breakpoints — widen the track for four long currency
figures, narrow the rail for a sidebar-heavy page.Concepts
- Window scope versus sample scope — the tiles and the chart describe every run in the window; the table is only the most recent handful. The block says so in a sentence and repeats it whenever a filter is on, because the alternative is someone summing twelve rows during an incident and concluding the dashboard is broken.
- A filter that must not move the numbers above it — picking an agent in the rail narrows the sample and nothing else. Recomputing the KPI row from the filtered rows would look helpful and would silently answer a different question than the one the tiles are labelled with.
- Derived trends with a per-metric good direction — the delta is computed from
valueandpreviousrather than handed over, so it can never contradict the figure beside it, and each metric carries its own opinion about which way is good news. Run count deliberately has none. - Quiet is not empty — a workspace that ran nothing today is
readywith zeros and a flat chart, not an empty state; an empty state means nothing has ever run here. They read differently and they lead to different next actions. - No disclosure without a payload — only rows that actually carry an error get a chevron, and the detail row exists in the DOM while hidden so
aria-controlsnever points at nothing. A failed row with no error text says so instead of offering a control that opens onto blank space. - Clamped stacking, one model — a bucket's failures are clamped inside its run count so no bar can outgrow the number it reports, and the same derived model feeds the bars, the caption and the accessible label, which is what stops the picture and its text alternative from drifting apart.
Research Report
The finished output of a deep-research run — prose whose citation numbers are derived from one walk of the data, per-section source chips, an outline that follows the reader, and a methodology footer that shows the queries run and the sources rejected.
Assistant Welcome
A first-run assistant hero — greeting, capability cards and starter chips that prefill the host's composer instead of sending, over a token-only animated accent.