AI

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import {
  ArrowDown,
  ArrowUp,
  Ban,
  Check,
  ChevronRight,
  CircleCheck,
  Coins,
  Copy,
  CopyX,
  Filter,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/agent-dashboard.json

Prompt

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 value and previous rather 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 ready with 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-controls never 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.

On This Page