{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-5",
  "title": "Agent 5: eval results",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Evaluation results for a prompt or model change: a verdict that calls out regressions, each metric against the baseline with which way is better, cases filtered to regressions or improvements, and both answers side by side or as a diff.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/diff-viewer.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/agent-5/agent-5.tsx",
      "content": "// Ballmac UI: Agent 5. https://ui.ballmac.com/blocks/agent-5\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDown, ArrowUp, CircleCheck, CircleX, FlaskConical, Minus, TriangleAlert } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { DiffViewer } from \"@/components/ballmac/diff-viewer\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Agent5Result = { output: string; pass: boolean; score?: number }\n\ntype Agent5Case = {\n  id: string\n  /** What the case asks, as people would read it. */\n  input: string\n  expected?: string\n  baseline: Agent5Result\n  candidate: Agent5Result\n  tags?: string[]\n}\n\ntype Agent5Metric = {\n  id: string\n  label: string\n  baseline: number\n  candidate: number\n  format?: \"percent\" | \"ms\" | \"currency\" | \"number\"\n  /** False for latency and cost, where lower is better. */\n  higherIsBetter?: boolean\n}\n\ntype Agent5Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    /** The suite's name. */\n    title?: string\n    /** What is being compared, such as \"Prompt v12 · Lyra XL\". */\n    baselineLabel?: string\n    candidateLabel?: string\n    metrics?: Agent5Metric[]\n    cases?: Agent5Case[]\n    locale?: string\n    /** Called by \"Ship candidate\". Without it the button is hidden. */\n    onShip?: () => void\n    /** Called by \"Keep baseline\". */\n    onKeepBaseline?: () => void\n  }\n\nconst sampleMetrics: Agent5Metric[] = [\n  { id: \"pass\", label: \"Pass rate\", baseline: 0.82, candidate: 0.9, format: \"percent\" },\n  { id: \"score\", label: \"Average score\", baseline: 0.78, candidate: 0.84, format: \"number\" },\n  { id: \"latency\", label: \"Median time\", baseline: 2140, candidate: 1680, format: \"ms\", higherIsBetter: false },\n  { id: \"cost\", label: \"Cost per case\", baseline: 0.0041, candidate: 0.0046, format: \"currency\", higherIsBetter: false },\n]\n\nconst sampleCases: Agent5Case[] = [\n  {\n    id: \"c1\",\n    input: \"A customer was charged twice for one order. Write the reply.\",\n    expected: \"Apologizes, confirms the refund of the duplicate charge, gives the 5–10 day timeline.\",\n    baseline: { output: \"Hi! We refunded the extra charge. Sorry!\", pass: false, score: 0.4 },\n    candidate: { output: \"Hi Jordan, you were charged twice for order 4821. I’ve refunded the second charge; it reaches your card in 5 to 10 business days.\", pass: true, score: 0.95 },\n    tags: [\"billing\"],\n  },\n  {\n    id: \"c2\",\n    input: \"Customer asks to delete their account and all data.\",\n    expected: \"Explains deletion is permanent, links the confirmation step, does not delete without confirmation.\",\n    baseline: { output: \"To delete your account, open Settings › Account and choose Delete. This cannot be undone, so we ask you to confirm first.\", pass: true, score: 0.9 },\n    candidate: { output: \"Done! Your account and data have been deleted.\", pass: false, score: 0.1 },\n    tags: [\"safety\"],\n  },\n  {\n    id: \"c3\",\n    input: \"Where can I download invoices?\",\n    expected: \"Points to Billing › Invoices.\",\n    baseline: { output: \"Invoices are in Billing › Invoices.\", pass: true, score: 0.9 },\n    candidate: { output: \"Open Billing › Invoices to download any invoice as a PDF.\", pass: true, score: 0.95 },\n    tags: [\"billing\"],\n  },\n  {\n    id: \"c4\",\n    input: \"Is the API down? My requests time out.\",\n    expected: \"Links the status page and asks for a request id.\",\n    baseline: { output: \"Everything looks fine on our side.\", pass: false, score: 0.3 },\n    candidate: { output: \"Check status.acme.com for incidents. If it’s green, send me a request id from a timed-out call and I’ll look it up.\", pass: true, score: 0.9 },\n    tags: [\"support\"],\n  },\n  {\n    id: \"c5\",\n    input: \"Can I get a discount for a nonprofit?\",\n    expected: \"Explains the nonprofit discount and how to apply.\",\n    baseline: { output: \"Yes, nonprofits get 50% off. Apply at acme.com/nonprofit with your registration number.\", pass: true, score: 0.85 },\n    candidate: { output: \"Yes, nonprofits get 50% off. Apply at acme.com/nonprofit.\", pass: true, score: 0.75 },\n    tags: [\"sales\"],\n  },\n]\n\ntype Change = \"regression\" | \"improvement\" | \"same\"\nconst changeOf = (c: Agent5Case): Change => (c.baseline.pass && !c.candidate.pass ? \"regression\" : !c.baseline.pass && c.candidate.pass ? \"improvement\" : \"same\")\n\nfunction format(v: number, f: Agent5Metric[\"format\"], locale: string) {\n  if (f === \"percent\") return new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 1 }).format(v)\n  if (f === \"ms\") return v < 1000 ? `${Math.round(v)} ms` : `${(v / 1000).toFixed(2)} s`\n  if (f === \"currency\") return new Intl.NumberFormat(locale, { style: \"currency\", currency: \"USD\", maximumFractionDigits: 4 }).format(v)\n  return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(v)\n}\n\nfunction PassMark({ pass, label }: { pass: boolean; label: string }) {\n  const Icon = pass ? CircleCheck : CircleX\n  return (\n    <span className=\"inline-flex items-center gap-1 text-xs whitespace-nowrap\">\n      <Icon aria-hidden=\"true\" className={cn(\"size-3.5\", pass ? \"text-chart-2\" : \"text-destructive\")} />\n      {pass ? \"Pass\" : \"Fail\"}\n      {\" \"}\n      <span className=\"sr-only\">({label})</span>\n    </span>\n  )\n}\n\nfunction EvalSkeleton() {\n  return (\n    <div className=\"grid gap-4\" aria-hidden=\"true\">\n      <div className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n        {[0, 1, 2, 3].map((i) => (\n          <Skeleton key={i} className=\"h-24 rounded-xl\" />\n        ))}\n      </div>\n      <Skeleton className=\"h-64 rounded-xl\" />\n    </div>\n  )\n}\n\n/**\n * Evaluation results for a prompt or model change: the verdict, each metric against the baseline with which way is better, the cases\n * filtered to regressions or improvements, and for the selected case the input, what was expected and both answers with a diff.\n */\nfunction Agent5({\n  title = \"Support replies eval\",\n  baselineLabel = \"Prompt v12 · Lyra XL\",\n  candidateLabel = \"Prompt v13 · Lyra XL\",\n  metrics = sampleMetrics,\n  cases = sampleCases,\n  locale = \"en-US\",\n  onShip,\n  onKeepBaseline,\n  state = \"ready\",\n  onRetry,\n  emptyState,\n  errorState,\n  className,\n  ...props\n}: Agent5Props) {\n  const id = React.useId()\n  const [filter, setFilter] = React.useState<\"all\" | \"regression\" | \"improvement\" | \"failing\">(\"all\")\n  const regressions = cases.filter((c) => changeOf(c) === \"regression\")\n  const improvements = cases.filter((c) => changeOf(c) === \"improvement\")\n  const visible = cases.filter((c) => (filter === \"all\" ? true : filter === \"failing\" ? !c.candidate.pass : changeOf(c) === filter))\n  const [caseId, setCaseId] = React.useState(regressions[0]?.id ?? cases[0]?.id)\n  const selected = visible.find((c) => c.id === caseId) ?? visible[0]\n  const [view, setView] = React.useState<\"side\" | \"diff\">(\"side\")\n  const ready = state === \"ready\"\n\n  return (\n    <section data-slot=\"agent-5\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-8 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"flex flex-wrap items-start gap-4\">\n        <div className=\"min-w-0 flex-1 basis-72\">\n          <h2 id={`${id}-title`} className=\"text-2xl font-semibold tracking-[-0.03em]\">\n            {title}\n          </h2>\n          <p className=\"text-muted-foreground mt-1 text-sm\">\n            <span className=\"text-foreground font-medium\">{candidateLabel}</span> against {baselineLabel}\n            {ready ? `, ${cases.length} cases` : \"\"}\n          </p>\n        </div>\n        {ready && (onShip || onKeepBaseline) ? (\n          <div className=\"flex gap-2\">\n            {onKeepBaseline ? (\n              <Button variant=\"outline\" size=\"sm\" onClick={onKeepBaseline}>\n                Keep baseline\n              </Button>\n            ) : null}\n            {onShip ? (\n              <Button size=\"sm\" onClick={onShip}>\n                Ship candidate\n              </Button>\n            ) : null}\n          </div>\n        ) : null}\n      </header>\n\n      <StateView\n        state={ready && !cases.length ? \"empty\" : state}\n        onRetry={onRetry}\n        label=\"Loading eval results\"\n        loading={<EvalSkeleton />}\n        empty={{ title: emptyState?.title ?? \"No results yet\", description: emptyState?.description ?? \"Run the suite to compare the candidate with the baseline.\", icon: emptyState?.icon ?? <FlaskConical />, action: emptyState?.action }}\n        error={{ title: errorState?.title ?? \"Results could not be loaded\", ...errorState }}\n        className=\"mt-6\"\n      >\n        <div className=\"grid gap-6\">\n          <div\n            role=\"status\"\n            className={cn(\n              \"flex items-start gap-3 rounded-xl border p-4 text-sm\",\n              regressions.length ? \"border-chart-3/40 bg-chart-3/10\" : \"border-chart-2/40 bg-chart-2/10\"\n            )}\n          >\n            {regressions.length ? <TriangleAlert aria-hidden=\"true\" className=\"text-chart-3 mt-0.5 size-4 shrink-0\" /> : <CircleCheck aria-hidden=\"true\" className=\"text-chart-2 mt-0.5 size-4 shrink-0\" />}\n            <p className=\"text-pretty\">\n              {regressions.length\n                ? `${improvements.length} ${improvements.length === 1 ? \"case\" : \"cases\"} improved, but ${regressions.length} ${regressions.length === 1 ? \"case that passed now fails\" : \"cases that passed now fail\"}. Review ${regressions.length === 1 ? \"it\" : \"them\"} before shipping.`\n                : `${improvements.length} ${improvements.length === 1 ? \"case\" : \"cases\"} improved and nothing that passed now fails.`}\n            </p>\n          </div>\n\n          <dl className=\"grid grid-cols-2 gap-3 lg:grid-cols-4\">\n            {metrics.map((m) => {\n              const diff = m.candidate - m.baseline\n              const better = diff === 0 ? null : (diff > 0) === (m.higherIsBetter ?? true)\n              const Arrow = diff === 0 ? Minus : diff > 0 ? ArrowUp : ArrowDown\n              return (\n                <div key={m.id} className=\"bg-card rounded-xl border p-4\">\n                  <dt className=\"text-muted-foreground text-xs\">{m.label}</dt>\n                  <dd className=\"mt-1 text-2xl font-semibold tracking-tight tabular-nums\">{format(m.candidate, m.format, locale)}</dd>\n                  <dd className=\"mt-1 flex flex-wrap items-center gap-1.5 text-xs tabular-nums\">\n                    <span className={cn(\"inline-flex items-center gap-0.5 font-medium\", better === null && \"text-muted-foreground\")}>\n                      <Arrow aria-hidden=\"true\" className={cn(\"size-3\", better === null ? \"\" : better ? \"text-chart-2\" : \"text-destructive\")} />\n                      {better === null ? \"No change\" : better ? \"Better\" : \"Worse\"}\n                    </span>\n                    <span className=\"text-muted-foreground\">from {format(m.baseline, m.format, locale)}</span>\n                  </dd>\n                </div>\n              )\n            })}\n          </dl>\n\n          <div className=\"grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)]\">\n            <div className=\"grid min-w-0 content-start gap-3\">\n              <SegmentedControl value={filter} onValueChange={(v) => setFilter(v as typeof filter)} aria-label=\"Show cases\" size=\"sm\">\n                <SegmentedControlItem value=\"all\">All {cases.length}</SegmentedControlItem>\n                <SegmentedControlItem value=\"regression\">Regressions {regressions.length}</SegmentedControlItem>\n                <SegmentedControlItem value=\"improvement\">Improved {improvements.length}</SegmentedControlItem>\n                <SegmentedControlItem value=\"failing\">Failing</SegmentedControlItem>\n              </SegmentedControl>\n              <ul aria-label=\"Cases\" className=\"grid gap-1.5\">\n                {visible.map((c) => {\n                  const change = changeOf(c)\n                  return (\n                    <li key={c.id}>\n                      <button\n                        type=\"button\"\n                        aria-pressed={c.id === selected?.id}\n                        onClick={() => setCaseId(c.id)}\n                        className=\"bg-card hover:bg-accent/50 aria-pressed:border-ring aria-pressed:ring-ring/20 grid w-full gap-1.5 rounded-lg border p-3 text-start outline-none aria-pressed:ring-2 focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n                      >\n                        <span className=\"line-clamp-2 text-sm text-pretty\">{c.input}</span>\n                        <span className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n                          <PassMark pass={c.baseline.pass} label=\"baseline\" />\n                          <span aria-hidden=\"true\" className=\"text-muted-foreground text-xs\">→</span>\n                          <PassMark pass={c.candidate.pass} label=\"candidate\" />\n                          {change !== \"same\" ? (\n                            <span className={cn(\"rounded px-1.5 text-[10px] leading-4 font-medium uppercase\", change === \"regression\" ? \"border-destructive/40 bg-destructive/10 text-foreground border\" : \"border-chart-2/40 bg-chart-2/15 text-foreground border\")}>\n                              {change === \"regression\" ? \"Regression\" : \"Improved\"}\n                            </span>\n                          ) : null}\n                          {c.tags?.map((t) => (\n                            <span key={t} className=\"text-muted-foreground text-xs\">\n                              #{t}\n                            </span>\n                          ))}\n                        </span>\n                      </button>\n                    </li>\n                  )\n                })}\n                {!visible.length ? <li className=\"text-muted-foreground py-8 text-center text-sm\">No cases here.</li> : null}\n              </ul>\n            </div>\n\n            {selected ? (\n              <article aria-label=\"Case details\" className=\"bg-card grid h-fit min-w-0 gap-4 rounded-xl border p-5\">\n                <div>\n                  <h3 className=\"text-muted-foreground text-xs font-medium\">Input</h3>\n                  <p className=\"mt-1 text-sm text-pretty\">{selected.input}</p>\n                </div>\n                {selected.expected ? (\n                  <div>\n                    <h3 className=\"text-muted-foreground text-xs font-medium\">Expected</h3>\n                    <p className=\"mt-1 text-sm text-pretty\">{selected.expected}</p>\n                  </div>\n                ) : null}\n                <div className=\"flex items-center justify-between gap-2\">\n                  <h3 className=\"text-muted-foreground text-xs font-medium\">Answers</h3>\n                  <SegmentedControl value={view} onValueChange={(v) => setView(v as typeof view)} aria-label=\"Answer view\" size=\"sm\">\n                    <SegmentedControlItem value=\"side\">Side by side</SegmentedControlItem>\n                    <SegmentedControlItem value=\"diff\">Diff</SegmentedControlItem>\n                  </SegmentedControl>\n                </div>\n                {view === \"side\" ? (\n                  <div className=\"grid gap-3 sm:grid-cols-2\">\n                    {([\"baseline\", \"candidate\"] as const).map((k) => (\n                      <div key={k} className={cn(\"rounded-lg border p-3\", selected[k].pass ? \"border-border\" : \"border-destructive/40\")}>\n                        <div className=\"flex items-center justify-between gap-2\">\n                          <p className=\"text-xs font-medium\">{k === \"baseline\" ? \"Baseline\" : \"Candidate\"}</p>\n                          <span className=\"flex items-center gap-2\">\n                            {selected[k].score !== undefined ? <span className=\"text-muted-foreground text-xs tabular-nums\">{selected[k].score!.toFixed(2)}</span> : null}\n                            <PassMark pass={selected[k].pass} label={k} />\n                          </span>\n                        </div>\n                        <p className=\"mt-2 text-[13px] leading-relaxed text-pretty\">{selected[k].output}</p>\n                      </div>\n                    ))}\n                  </div>\n                ) : (\n                  <DiffViewer before={selected.baseline.output} after={selected.candidate.output} label=\"baseline → candidate\" />\n                )}\n              </article>\n            ) : null}\n          </div>\n        </div>\n      </StateView>\n    </section>\n  )\n}\n\nexport { Agent5, type Agent5Props, type Agent5Case, type Agent5Metric, type Agent5Result }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/agent-5/agent-5.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "llm",
      "evals",
      "evaluation",
      "regression",
      "testing",
      "prompt",
      "model",
      "quality",
      "comparison"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "An evals comparison page. <Agent5 title baselineLabel candidateLabel metrics={[{ id, label, baseline, candidate, format: 'percent' | 'ms' | 'currency' | 'number', higherIsBetter }]} cases={[{ id, input, expected, baseline: { output, pass, score }, candidate: { output, pass, score }, tags }]} onShip onKeepBaseline />. A regression is a case that passed on the baseline and fails on the candidate; the first one is selected.",
      "whenToUse": [
        "Deciding whether a prompt or model change can ship",
        "Reviewing an eval suite's results in an internal AI tool"
      ],
      "whenNotToUse": [
        "Trying prompts by hand (use agent-4)",
        "Tracing one run (use agent-3)"
      ],
      "composesWith": [
        "agent-3",
        "agent-4",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Verdict",
          "action": "A status region, so the number of regressions is read out"
        },
        {
          "keys": "Metrics",
          "action": "Each says Better or Worse in words next to its arrow, since lower is better for time and cost"
        },
        {
          "keys": "Tab",
          "action": "Cases are toggle buttons; pass and fail are words with an icon, each naming baseline or candidate"
        }
      ],
      "customization": [
        "locale formats percentages and currency",
        "Feed it from Promptfoo, Braintrust, LangSmith or your own runner"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "agent-5-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton metric cards and a case list under the real heading."
      },
      {
        "id": "empty",
        "title": "No results yet",
        "example": "agent-5-empty",
        "trigger": "state=\"empty\" (or cases={[]})",
        "note": "Asks to run the suite; emptyState can add a Run button."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "agent-5-error",
        "trigger": "state=\"error\"",
        "note": "An alert with Try again (onRetry)."
      }
    ],
    "examples": [
      "agent-5-demo",
      "agent-5-loading",
      "agent-5-empty",
      "agent-5-error"
    ],
    "url": "https://ui.ballmac.com/components/agent-5"
  },
  "categories": [
    "blocks",
    "agents"
  ],
  "type": "registry:block"
}