Review and approveEval results
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callEval results
An eval run case by case, against the run before. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.
No failed cases
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add eval-resultsReact
npx designme add eval-results --reactThe CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.
About
What it is for and what it does.
Eval results shows an eval suite's latest run case by case. A bar with one segment per case fills as the cases run, the pass count is compared with the run before, and a filter narrows the list to the failed cases. A failed case opens to show what was expected, what came back and where the answer comes from, with a fix that runs it again.
- When an agent's output is checked by a set of evals and people need to see which ones failed, not just a score.
- When a change to a prompt, a model or the agent's work should be compared with the run before, to catch a regression.
- When a failure needs its evidence, expected against what came back, to be fixed.
- When the suite is long and people only want the failures.
casesis the suite. Each case has aname, astatusofqueued,running,passorfail, and atimeshown once it has a result.- The bar has one segment per case: grey while queued, pulsing while running, green for a pass and red for a fail.
- The count reads "Running 3 of 8" while
running, then "7 of 8 passed". WithpreviousPassed, a tag compares the settled run with the one before, like "1 fewer than the last run", or reads All passing. onRunadds a filled run button, labelled byrunLabel; it spins whilerunning.- All and Failed switch the list, with the count on each; the cases that don't match fold away. With no failures, Failed says "No failed cases".
- A failed case is red-tinted and opens to show
expected,gotin red andsource, with an optional whitefixbutton, like Use $24 and rerun.
- The cases form a list labelled "Cases". A failed case is a button with
aria-expanded, and its details fold away inert when it's shut. - All and Failed are a labelled group of toggle buttons with
aria-pressed. - The pass count is announced politely as it changes.
- The bar is decoration; the count says the same in words.
Usage
Import it and drive it from your own state.
Import
import { EvalResults } from '@/components/designme/eval-results/EvalResults';Example
The demo above as a React component, 58 lines you can copy.
'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useRef, useState } from 'react';
import { EvalResults, type EvalCase } from './EvalResults';
import { useStory, useWait } from '../lib/useScript';
/* ---------- Eval results: run the evals, see one fail against the last run, open it, take the fix, and see all eight pass ---------- */
// eight checks on the pricing page's copy. The price check fails after the copy agent's latest edit, until it's fixed
const CASES: EvalCase[] = [
{ id: 'seat', name: 'Names the Pro seat limit', time: '0.6s', status: 'pass' },
{ id: 'price', name: 'Prices match the billing table', time: '0.9s', status: 'pass', expected: '$24 a seat, billed yearly', got: '$29 a seat, billed yearly', source: 'Billing table, Pro row' },
{ id: 'yearly', name: 'Mentions the yearly discount', time: '0.5s', status: 'pass' },
{ id: 'unl', name: 'Makes no “unlimited” claims', time: '0.4s', status: 'pass' },
{ id: 'sso', name: 'Team plan lists SSO', time: '0.5s', status: 'pass' },
{ id: 'grade', name: 'Reads at grade 8 or below', time: '0.7s', status: 'pass' },
{ id: 'cta', name: 'One call to action per plan', time: '0.5s', status: 'pass' },
{ id: 'free', name: 'Free plan says no card is needed', time: '0.6s', status: 'pass' }
];
export default function EvalResultsDemo() {
const ref = useRef<HTMLDivElement>(null);
const wait = useWait();
const [cases, setCases] = useState(CASES);
const [running, setRunning] = useState(false);
const [ran, setRan] = useState(false);
const fixed = useRef(false);
const set = (id: string, status: EvalCase['status']) => setCases(cs => cs.map(k => k.id === id ? { ...k, status } : k));
// a run checks the cases one after another; the price check fails until the copy is fixed
const runAll = async () => {
setRunning(true); setRan(true); setCases(cs => cs.map(k => ({ ...k, status: 'queued' })));
for (const k of CASES) {
set(k.id, 'running'); await wait(500);
set(k.id, k.id === 'price' && !fixed.current ? 'fail' : 'pass');
}
setRunning(false);
};
// the fix changes the copy to the billing table's price and runs that case again
const fix = async () => { fixed.current = true; set('price', 'running'); await wait(1000); set('price', 'pass'); };
// the preview's story: run the evals, show the failed ones, open the price check and take its fix, then show them all
useStory(ref, async ({ wait, press }) => {
await wait(1500); press('.dm-evr-run');
await wait(4000 + 1500); press('.dm-seg2 button:last-of-type');
await wait(1000); press('.dm-evr-i.dm-fail .dm-evr-r');
await wait(1500); press('.dm-evr-fix');
await wait(2000); press('.dm-seg2 button:first-of-type');
});
return (
<div ref={ref} style={{ width: '100%' }}>
<EvalResults title="Pricing copy evals" subtitle={`Copy agent · 8 cases · run ${ran && !running ? 'just now' : '2h ago'}`}
cases={cases.map(k => k.id === 'price' && k.status === 'fail' ? { ...k, fix: { label: 'Use $24 and rerun', onClick: () => void fix() } } : k)}
onRun={() => void runAll()} running={running} runLabel={ran ? 'Run again' : 'Run evals'} previousPassed={ran ? 8 : undefined} />
</div>
);
}
Props
Everything EvalResults takes.
| Prop | Type | What it does |
|---|---|---|
titlerequired | string | Like "Pricing copy evals" |
subtitle | string | The line under the title, like "Copy agent · 8 cases · run 2h ago" |
casesrequired | EvalCase[] | |
onRun | () => void | Adds the run button |
running | boolean | Whether the whole suite is running: the button spins and the count reads "Running 3 of 8" |
runLabel | string | Defaults to "Run evals" |
previousPassed | number | How many passed the run before. Once the run settles, a tag compares with it, like "1 fewer than the last run" |
filter | EvalFilter | Which cases show: all of them, or only the failed ones |
defaultFilter | EvalFilter | |
onFilterChange | (filter: EvalFilter) => void | |
open | string[] | Ids of the failed cases that are open. Leave it out to let each one open itself |
defaultOpen | string[] | |
onOpenChange | (ids: string[]) => void | |
className | string |
Code
The full source, as the CLI installs it.
EvalResults.tsxReact · 140 lines
'use client';
import { useState } from 'react';
import { cx } from '../lib/cx';
import { useControllable } from '../lib/hooks';
import { Icon } from '../lib/icons';
import { enter, Fold, Seg, Spin, Swap } from '../lib/motion';
import '../designme.css';
import './eval-results.css';
export type EvalStatus = 'queued' | 'running' | 'pass' | 'fail';
export type EvalFilter = 'all' | 'fail';
export interface EvalCase {
id: string;
/** What it checks, like "Prices match the billing table" */
name: string;
status: EvalStatus;
/** How long it took, like "0.9s", shown once it has a result */
time?: string;
/** For a failed case: what should have come back */
expected?: string;
/** For a failed case: what came back instead */
got?: string;
/** For a failed case: where the expected answer comes from */
source?: string;
/** For a failed case: a white button under its details, like Use $24 and rerun */
fix?: { label: string; onClick: () => void };
}
export interface EvalResultsProps {
/** Like "Pricing copy evals" */
title: string;
/** The line under the title, like "Copy agent · 8 cases · run 2h ago" */
subtitle?: string;
cases: EvalCase[];
/** Adds the run button */
onRun?: () => void;
/** Whether the whole suite is running: the button spins and the count reads "Running 3 of 8" */
running?: boolean;
/** Defaults to "Run evals" */
runLabel?: string;
/** How many passed the run before. Once the run settles, a tag compares with it, like "1 fewer than the last run" */
previousPassed?: number;
/** Which cases show: all of them, or only the failed ones */
filter?: EvalFilter;
defaultFilter?: EvalFilter;
onFilterChange?: (filter: EvalFilter) => void;
/** Ids of the failed cases that are open. Leave it out to let each one open itself */
open?: string[];
defaultOpen?: string[];
onOpenChange?: (ids: string[]) => void;
className?: string;
}
/** How many times `value` has changed since the first render, so a glyph can play its entrance on a change but not on mount */
function useChanges<T>(value: T) {
const [seen, setSeen] = useState({ value, n: 0 });
if (Object.is(seen.value, value)) return seen.n;
setSeen({ value, n: seen.n + 1 });
return seen.n + 1;
}
function Glyph({ status }: { status: EvalStatus }) {
const e = useChanges(status) > 0 ? enter() : undefined;
return (
<span key={status} className={cx('dm-evr-g', e?.className)} style={e?.style} aria-hidden="true">
{status === 'running' ? <Spin /> : status === 'pass' ? <Icon name="ok" size="xs" /> : status === 'fail' ? <Icon name="alert" size="xs" /> : <span className="dm-dot" />}
</span>
);
}
const SEG: Record<EvalStatus, string | undefined> = { queued: undefined, running: 'dm-run', pass: 'dm-pass', fail: 'dm-fail' };
/** An eval suite's latest run: a bar segment per case, the pass count against the run before, and the cases, all or only
the failed ones. A failed case opens to show what was expected and what came back, with a fix. */
export function EvalResults({ title, subtitle, cases, onRun, running = false, runLabel = 'Run evals', previousPassed, filter, defaultFilter = 'all', onFilterChange, open, defaultOpen = [], onOpenChange, className }: EvalResultsProps) {
const [show, setShow] = useControllable(filter, defaultFilter, onFilterChange);
const [opened, setOpened] = useControllable(open, defaultOpen, onOpenChange);
const passed = cases.filter(k => k.status === 'pass').length, failed = cases.filter(k => k.status === 'fail').length;
const settled = cases.every(k => k.status === 'pass' || k.status === 'fail');
const count = settled ? `${passed} of ${cases.length} passed` : running ? `Running ${passed + failed + 1} of ${cases.length}` : 'Running the case again';
const delta = !settled || running || previousPassed === undefined ? '' : passed === cases.length ? 'All passing'
: passed < previousPassed ? `${previousPassed - passed} fewer than the last run` : passed > previousPassed ? `${passed - previousPassed} more than the last run` : '';
const dl = delta ? enter() : undefined;
return (
<div data-dm="" className={cx('dm-evr dm-panel', className)}>
<div className="dm-evr-h">
<div className="dm-evr-ht"><b>{title}</b>{subtitle && <Swap className="dm-muted dm-evr-st">{subtitle}</Swap>}</div>
{onRun && (
<button type="button" className="dm-pill dm-primary dm-sm dm-evr-run" disabled={running} onClick={onRun}>
{running ? <><Spin />Running</> : <><Icon name="play" size="xs" />{runLabel}</>}
</button>
)}
</div>
<div className="dm-evr-sum">
<div className="dm-evr-segs" aria-hidden="true">{cases.map(k => <i key={k.id} className={SEG[k.status]} />)}</div>
<div className="dm-evr-nums">
<span aria-live="polite"><Swap className="dm-evr-pass dm-num">{count}</Swap></span>
{delta && <span key={delta} className={cx('dm-tag dm-evr-dl', passed < (previousPassed ?? 0) ? 'dm-bad' : 'dm-ok', dl?.className)} style={dl?.style}>{delta}</span>}
</div>
</div>
<div className="dm-evr-tabs">
<Seg label="Show" value={show} onChange={setShow} options={[
{ value: 'all', label: <>All <span className="dm-n dm-num">{cases.length}</span></> },
{ value: 'fail', label: <>Failed <span className="dm-n dm-num">{failed}</span></> }
]} />
</div>
<div className="dm-evr-list" role="list" aria-label="Cases">
{cases.map(k => {
const fail = k.status === 'fail', isOpen = fail && opened.includes(k.id);
const meta = <><Glyph status={k.status} /><span className="dm-evr-n">{k.name}</span><span className="dm-evr-tm dm-num">{k.status === 'pass' || fail ? k.time : ''}</span></>;
return (
<Fold key={k.id} open={show === 'all' || fail} className="dm-evr-w">
<div className={cx('dm-evr-i', `dm-${k.status}`)} role="listitem">
{fail ? (
<>
<button type="button" className="dm-evr-r" aria-expanded={isOpen} onClick={() => setOpened(isOpen ? opened.filter(id => id !== k.id) : [...opened, k.id])}>
{meta}<span className="dm-evr-ch"><Icon name="chev-d" size="xs" /></span>
</button>
<Fold open={isOpen} className="dm-evr-d">
<div className="dm-evr-dd">
{k.expected && <span className="dm-evr-kv"><span className="dm-k">Expected</span><span>{k.expected}</span></span>}
{k.got && <span className="dm-evr-kv"><span className="dm-k">Got</span><span className="dm-bad">{k.got}</span></span>}
{k.source && <span className="dm-evr-kv"><span className="dm-k">Source</span><span>{k.source}</span></span>}
{k.fix && <button type="button" className="dm-pill dm-sm dm-evr-fix" onClick={k.fix.onClick}>{k.fix.label}</button>}
</div>
</Fold>
</>
) : <div className="dm-evr-r">{meta}</div>}
</div>
</Fold>
);
})}
</div>
{show === 'fail' && !failed && <p className="dm-evr-none">No failed cases</p>}
</div>
);
}
eval-results.cssReact · 48 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Eval results ---------- */
/* an eval suite's latest run: one bar segment per case, the pass count against the run before, and the cases, all or
just the failed ones. A failed case opens to show what was expected and what came back, with a fix that reruns it */
.dm-evr{width:100%;max-width:440px;margin:0 auto;padding:6px;display:flex;flex-direction:column}
.dm-evr-h{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-3);padding:var(--dm-s-2) 10px 10px}
.dm-evr-ht{display:flex;flex-direction:column;min-width:0}
.dm-evr-ht b{font-weight:500;font-size:14px}
.dm-evr-st{font-size:12.5px}
.dm-evr-run{flex:none}
.dm-evr-run .dm-spin{width:10px;height:10px}
.dm-evr-sum{display:flex;flex-direction:column;gap:var(--dm-s-2);padding:var(--dm-s-3) 10px;border-top:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent);border-bottom:1px solid color-mix(in srgb,var(--dm-line) 50%,transparent)}
.dm-evr-segs{display:flex;gap:3px}
.dm-evr-segs i{flex:1;height:6px;border-radius:3px;background:var(--dm-rail);transition:background .3s}
.dm-evr-segs i.dm-pass{background:var(--dm-ok)}
.dm-evr-segs i.dm-fail{background:var(--dm-bad)}
.dm-evr-segs i.dm-run{background:var(--dm-accent-mark,var(--dm-accent));animation:dm-evrrun 1s ease-in-out infinite}
@keyframes dm-evrrun{50%{opacity:.4}}
.dm-evr-nums{display:flex;align-items:center;justify-content:space-between;gap:var(--dm-s-2);min-height:20px;font-size:13px}
.dm-evr-dl:empty{display:none}
.dm-evr-tabs{padding:10px var(--dm-s-1) var(--dm-s-1)}
.dm-evr-tabs .dm-n{color:var(--dm-muted);margin-left:2px}
.dm-evr-tabs .dm-n:empty{display:none}
.dm-evr-list{display:flex;flex-direction:column}
/* a case folds away when the filter hides it, and stays hidden once shut so nothing measures it */
.dm-evr-w.dm-shut>div{visibility:hidden;transition:visibility 0s var(--dm-t)}
.dm-evr-i{display:flex;flex-direction:column;border-radius:10px;transition:background .2s}
.dm-evr-r{display:flex;align-items:center;gap:10px;width:100%;min-height:38px;padding:6px 10px;border-radius:10px;font-size:13px;text-align:left;color:var(--dm-fg)}
.dm-evr-g{width:16px;height:16px;display:grid;place-items:center;flex:none;color:var(--dm-faint)}
.dm-evr-g .dm-spin{width:11px;height:11px;color:var(--dm-accent-fg,var(--dm-accent))}
.dm-evr-i.dm-pass .dm-evr-g{color:var(--dm-ok)}
.dm-evr-i.dm-fail .dm-evr-g{color:var(--dm-bad)}
.dm-evr-n{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-evr-i.dm-queued .dm-evr-n{color:var(--dm-fg2)}
.dm-evr-tm{font-size:12px;color:var(--dm-muted)}
.dm-evr-ch{display:grid;color:var(--dm-muted)}
.dm-evr-ch svg{transition:transform .32s var(--dm-ease)}
.dm-evr-r[aria-expanded="false"] .dm-evr-ch svg{transform:rotate(-90deg)}
.dm-evr-i.dm-fail{background:var(--dm-bad-bg)}.dm-evr-i.dm-fail :is(.dm-evr-tm,.dm-evr-kv .dm-k){color:var(--dm-fg2)}
.dm-evr-d.dm-shut>div{visibility:hidden;transition:visibility 0s var(--dm-t)}
.dm-evr-dd{display:flex;flex-direction:column;gap:var(--dm-s-1);padding:0 10px 10px 36px;font-size:12.5px}
.dm-evr-kv{display:flex;gap:var(--dm-s-2)}
.dm-evr-kv .dm-k{width:58px;flex:none;color:var(--dm-muted)}
.dm-evr-kv .dm-bad{color:var(--dm-bad)}
.dm-evr-fix{align-self:flex-start;margin-top:6px;background:var(--dm-surface);color:var(--dm-fg);box-shadow:var(--dm-shadow)}
@media (hover:hover) and (pointer:fine){.dm-evr-fix:hover{background:var(--dm-surface);box-shadow:var(--dm-shadow-pop)}}
.dm-evr-none{margin:0;padding:10px;font-size:12.5px;color:var(--dm-muted)}
eval-results.htmlHTML · 12 lines
<div class="card" data-c="eval-results">
<div class="stage-in">
<div class="evr panel">
<div class="evr-h"><div class="evr-ht"><b>Pricing copy evals</b><span class="muted evr-st">Copy agent · 8 cases · run 2h ago</span></div><button class="pill primary sm evr-run"><svg class="i xs"><use href="#i-play"/></svg>Run evals</button></div>
<div class="evr-sum"><div class="evr-segs" aria-hidden="true"></div><div class="evr-nums"><span class="evr-pass num" aria-live="polite"></span><span class="tag evr-dl"></span></div></div>
<div class="evr-tabs"><div class="seg2" role="group" aria-label="Show"><button aria-pressed="true" data-v="all">All <span class="n num"></span></button><button aria-pressed="false" data-v="fail">Failed <span class="n num"></span></button></div></div>
<div class="evr-list" role="list" aria-label="Cases"></div>
<p class="evr-none" hidden>No failed cases</p>
</div>
</div>
</div>
eval-results.cssHTML · 47 lines
/* ---------- Eval results ---------- */
/* an eval suite's latest run: one bar segment per case, the pass count against the run before, and the cases, all or
just the failed ones. A failed case opens to show what was expected and what came back, with a fix that reruns it */
.evr{width:100%;max-width:440px;margin:0 auto;padding:6px;display:flex;flex-direction:column}
.evr-h{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);padding:var(--s-2) 10px 10px}
.evr-ht{display:flex;flex-direction:column;min-width:0}
.evr-ht b{font-weight:500;font-size:14px}
.evr-st{font-size:12.5px}
.evr-run{flex:none}
.evr-run .spin{width:10px;height:10px}
.evr-sum{display:flex;flex-direction:column;gap:var(--s-2);padding:var(--s-3) 10px;border-top:1px solid color-mix(in srgb,var(--line) 50%,transparent);border-bottom:1px solid color-mix(in srgb,var(--line) 50%,transparent)}
.evr-segs{display:flex;gap:3px}
.evr-segs i{flex:1;height:6px;border-radius:3px;background:var(--rail);transition:background .3s}
.evr-segs i.pass{background:var(--ok)}
.evr-segs i.fail{background:var(--bad)}
.evr-segs i.run{background:var(--accent-mark,var(--accent));animation:evrrun 1s ease-in-out infinite}
@keyframes evrrun{50%{opacity:.4}}
.evr-nums{display:flex;align-items:center;justify-content:space-between;gap:var(--s-2);min-height:20px;font-size:13px}
.evr-dl:empty{display:none}
.evr-tabs{padding:10px var(--s-1) var(--s-1)}
.evr-tabs .n{color:var(--muted);margin-left:2px}
.evr-tabs .n:empty{display:none}
.evr-list{display:flex;flex-direction:column}
/* a case folds away when the filter hides it, and stays hidden once shut so nothing measures it */
.evr-w.shut>div{visibility:hidden;transition:visibility 0s var(--t)}
.evr-i{display:flex;flex-direction:column;border-radius:10px;transition:background .2s}
.evr-r{display:flex;align-items:center;gap:10px;width:100%;min-height:38px;padding:6px 10px;border-radius:10px;font-size:13px;text-align:left;color:var(--fg)}
.evr-g{width:16px;height:16px;display:grid;place-items:center;flex:none;color:var(--faint)}
.evr-g .spin{width:11px;height:11px;color:var(--accent-fg,var(--accent))}
.evr-i.pass .evr-g{color:var(--ok)}
.evr-i.fail .evr-g{color:var(--bad)}
.evr-n{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.evr-i.queued .evr-n{color:var(--fg2)}
.evr-tm{font-size:12px;color:var(--muted)}
.evr-ch{display:grid;color:var(--muted)}
.evr-ch svg{transition:transform .32s var(--ease)}
.evr-r[aria-expanded="false"] .evr-ch svg{transform:rotate(-90deg)}
.evr-i.fail{background:var(--bad-bg)}.evr-i.fail :is(.evr-tm,.evr-kv .k){color:var(--fg2)}
.evr-d.shut>div{visibility:hidden;transition:visibility 0s var(--t)}
.evr-dd{display:flex;flex-direction:column;gap:var(--s-1);padding:0 10px 10px 36px;font-size:12.5px}
.evr-kv{display:flex;gap:var(--s-2)}
.evr-kv .k{width:58px;flex:none;color:var(--muted)}
.evr-kv .bad{color:var(--bad)}
.evr-fix{align-self:flex-start;margin-top:6px;background:var(--surface);color:var(--fg);box-shadow:var(--shadow)}
@media (hover:hover) and (pointer:fine){.evr-fix:hover{background:var(--surface);box-shadow:var(--shadow-pop)}}
.evr-none{margin:0;padding:10px;font-size:12.5px;color:var(--muted)}
eval-results.jsHTML · 85 lines
DM.def('eval-results', (r, c) => {
r.innerHTML = r.innerHTML.replace('{play}', I('play','xs'));
// eight checks on the pricing page's copy. The price check fails after the copy agent's latest edit, until it's fixed
const K = [
{ n:'Names the Pro seat limit', tm:'0.6s' },
{ n:'Prices match the billing table', tm:'0.9s', bad:true, exp:'$24 a seat, billed yearly', got:'$29 a seat, billed yearly', src:'Billing table, Pro row' },
{ n:'Mentions the yearly discount', tm:'0.5s' },
{ n:'Makes no “unlimited” claims', tm:'0.4s' },
{ n:'Team plan lists SSO', tm:'0.5s' },
{ n:'Reads at grade 8 or below', tm:'0.7s' },
{ n:'One call to action per plan', tm:'0.5s' },
{ n:'Free plan says no card is needed', tm:'0.6s' }
].map(k => ({ ...k, s:'pass' }));
const LAST = 8;
const list = $('.evr-list', r), segs = $('.evr-segs', r), pass = $('.evr-pass', r), dl = $('.evr-dl', r), st = $('.evr-st', r), run = $('.evr-run', r), none = $('.evr-none', r), sg = $('.evr-tabs .seg2', r);
const G = { run:'<span class="spin"></span>', pass:I('ok','xs'), fail:I('alert','xs'), queued:'<span class="dot"></span>' };
let filter = 'all', busy = false, ran = false;
K.forEach((k, i) => {
k.w = html(`<div class="fold evr-w"><div><div class="evr-i" role="listitem"></div></div></div>`); k.el = $('.evr-i', k.w); k.i = i; list.appendChild(k.w);
k.seg = document.createElement('i'); segs.appendChild(k.seg);
});
// a case's row: a failed one is a button that opens what was expected and what came back
function paint(k, fresh){
const open = k.s === 'fail' && k.open;
k.el.className = 'evr-i ' + k.s;
const g = `<span class="evr-g">${G[k.s]}</span><span class="evr-n">${k.n}</span><span class="evr-tm num">${k.s === 'pass' || k.s === 'fail' ? k.tm : ''}</span>`;
k.el.innerHTML = k.s === 'fail'
? `<button class="evr-r" aria-expanded="${!!open}">${g}<span class="evr-ch">${I('chev-d','xs')}</span></button><div class="fold evr-d${open ? '' : ' shut'}"><div><div class="evr-dd"><span class="evr-kv"><span class="k">Expected</span><span>${k.exp}</span></span><span class="evr-kv"><span class="k">Got</span><span class="bad">${k.got}</span></span><span class="evr-kv"><span class="k">Source</span><span>${k.src}</span></span><button class="pill sm evr-fix">Use $24 and rerun</button></div></div></div>`
: `<div class="evr-r">${g}</div>`;
if (k.s === 'fail'){
const d = $('.evr-d', k.el); d.inert = !open;
$('.evr-r', k.el).onclick = () => { k.open = !k.open; $('.evr-r', k.el).setAttribute('aria-expanded', k.open); d.classList.toggle('shut', !k.open); d.inert = !k.open; };
$('.evr-fix', k.el).onclick = () => fix(k);
}
if (fresh) enter($('.evr-g', k.el));
k.seg.className = k.s;
}
function counts(){
const p = K.filter(k => k.s === 'pass').length, f = K.filter(k => k.s === 'fail').length, done = K.every(k => k.s === 'pass' || k.s === 'fail');
swap(pass, done ? `${p} of ${K.length} passed` : busy ? `Running ${K.filter(k => k.s === 'pass' || k.s === 'fail').length + 1} of ${K.length}` : 'Running the case again');
const d = !ran || !done ? '' : p < LAST ? `${LAST - p} fewer than the last run` : 'All passing';
if (dl.textContent !== d){ dl.textContent = d; dl.className = 'tag evr-dl ' + (p < LAST ? 'bad' : 'ok'); if (d) enter(dl); }
$('[data-v="all"] .n', sg).textContent = K.length; $('[data-v="fail"] .n', sg).textContent = f;
show();
}
// All shows every case; Failed folds the others away
function show(){
K.forEach(k => { const on = filter === 'all' || k.s === 'fail'; k.w.classList.toggle('shut', !on); k.w.inert = !on; });
none.hidden = !(filter === 'fail' && !K.some(k => k.s === 'fail'));
}
seg(sg, v => { filter = v; show(); });
K.forEach(k => paint(k)); counts();
// a run checks the cases one after another; the price check fails until the copy is fixed
run.onclick = () => c.run(async () => {
if (busy) return; busy = true; ran = true; run.disabled = true; run.innerHTML = '<span class="spin"></span>Running';
K.forEach(k => { k.s = 'queued'; k.open = false; paint(k); }); counts();
for (const k of K){
k.s = 'run'; paint(k, true); counts();
await c.wait(500);
k.s = k.bad ? 'fail' : 'pass'; paint(k, true); counts();
}
busy = false; run.disabled = false; run.innerHTML = I('play','xs') + 'Run again'; swap(st, 'Copy agent · 8 cases · run just now');
});
// the fix changes the copy to the billing table's price and runs that case again
function fix(k){
if (busy) return;
c.run(async () => {
k.bad = false; k.open = false; k.s = 'run'; paint(k, true); counts();
await c.wait(1000);
k.s = 'pass'; paint(k, true); counts();
});
}
// the preview's story: run the evals, see one fail against the last run, open it, take the fix, and see all eight pass
c.story(async s => {
await s.beat(3);
await s.tap(run); s.away();
await s.until(() => !busy); await s.beat(3);
await s.tap($('[data-v="fail"]', sg)); s.away(); await s.beat(2);
await s.tap($('.evr-i.fail .evr-r', r)); s.away(); await s.beat(3);
await s.tap($('.evr-fix', r)); s.away();
await s.until(() => K.every(k => k.s === 'pass')); await s.beat(2);
await s.tap($('[data-v="all"]', sg)); s.away(); await s.beat(2);
});
});
Questions
What people ask about it.
- Does it run the evals?
- No.
onRuntells you to start. Set each case'sstatusas your eval runner reports it, andrunningfor the whole run. - What does the fix button do?
- Whatever you pass as its
onClick. In the demo it changes the copy to the billing table's price and runs that one case again. - Can it show scores instead of pass and fail?
- Not yet. Each case passes or fails; put a score in its
nameortimeif you need one there. - How does it compare with the last run?
- By the number that passed. Pass
previousPassedfrom your last run, and the tag appears once every case has a result.
More in Review and approve
Moments where a person checks, steers or signs off on the agent's work.