Skip to content

Comparison leaderboard UI consuming the /compare endpoint - #161

Open
lucastononro wants to merge 2 commits into
mainfrom
fix/105-compare-leaderboard
Open

Comparison leaderboard UI consuming the /compare endpoint#161
lucastononro wants to merge 2 commits into
mainfrom
fix/105-compare-leaderboard

Conversation

@lucastononro

@lucastononro lucastononro commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

The backend /compare endpoint (backend/routers/compare.py) already aggregates metrics, feature overlap, and cost totals across up to 8 sessions, but no frontend consumed it. This PR adds the comparison leaderboard UI: a sortable per-session table (metric columns + total cost + created-at), overlaid per-metric charts reusing the existing MetricsTab chart stack, a feature-overlap panel, and a "Compare selected" affordance on the experiments table.

Changes

  • frontend/src/lib/types.tsCompareResponse + CompareSessionInfo / CompareMetricSeries / CompareFeatureOverlap / CompareSessionTotals types mirroring routers/compare.py (including the quirk that feature_overlap is an empty list when no session has a prep summary).
  • frontend/src/lib/api.ts — typed api.compare(sessionIds) client fn (GET /api/compare?sessions=a,b,c via URLSearchParams).
  • frontend/src/components/MetricsTab.tsx — exported the reusable chart pieces (PALETTE, ChartTooltip, smartFormat, compactFormat, isLowerBetter, prettyMetricName); no behavior change.
  • frontend/src/app/compare/page.tsx (new) — /compare?sessions=…[&project=…] page inside the app shell (Sidebar + dark canvas):
    • Sortable leaderboard table: one row per session, one column per metric (latest logged value, best value highlighted; loss-like metrics rank lower-is-better), total cost, created-at. Click a header to sort; default sort ranks by the first metric, best first. Missing sessions render as stub rows.
    • Overlaid recharts line charts, one panel per metric, one colored series per session, with a session legend that toggles series across all charts (same visual language as MetricsTab).
    • Feature-overlap panel: features common to all sessions + per-session extras.
    • useSearchParams wrapped in a Suspense boundary so static prerendering works.
  • frontend/src/app/experiments/page.tsx — checkbox column (disabled for rows without a session) + a "Compare selected (n/8)" header button that navigates to /compare with the selected session ids, passing project= when the selection stays within one project. Selection is capped at the backend's 8-session limit.

Test plan

Existing

  • Experiments list still renders, groups by project, searches, and row-click still opens the experiment detail (checkbox clicks stop propagation).
  • Session metrics tab unchanged (MetricsTab default export untouched — only named exports added).
  • /compare backend API unchanged; no backend edits.

New (click-path)

  1. Open Experiments (/experiments).
  2. Tick the checkbox on 2+ rows that have sessions → a Compare selected (n/8) button appears in the header (disabled below 2; checkboxes disable beyond 8).
  3. Click it → lands on /compare?sessions=<id1>,<id2>[&project=<id>].
  4. Leaderboard renders one row per session with per-metric latest values (best per column highlighted in green), total cost, and created-at, ranked by the first metric best-first.
  5. Click Cost, Created, or any metric header → table re-sorts; clicking again flips direction (arrow indicator updates).
  6. Charts below overlay each metric across sessions; clicking a session in the legend hides/shows its series in all charts and dims its table row.
  7. Feature-overlap panel lists common features + per-session extras when prep summaries exist.
  8. Validated: npx tsc --noEmit, npm run lint, npm run build all green (/compare prerenders statically).

Closes #105

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4

Greptile Summary

This PR wires up the compare leaderboard UI against the already-shipped /compare backend endpoint, completing the feature requested in issue #105. A single api.compare(sessionIds) round-trip returns all data (session headers, per-metric series, feature overlap, cost totals) and the new /compare page renders a sortable leaderboard table, overlaid recharts line panels, a toggleable session legend, and a feature-overlap section — all reusing existing MetricsTab chart utilities.

  • frontend/src/app/compare/page.tsx (new): sortable table with per-column best-value highlighting (lower-is-better aware), overlaid recharts panels with per-session color assignment from PALETTE, a legend that toggles series across all charts simultaneously, and a feature-overlap panel that correctly disappears when feature_overlap is an empty list. A Suspense boundary wraps the inner component so useSearchParams doesn't block static prerendering.
  • frontend/src/app/experiments/page.tsx: checkbox column + "Compare selected (n/8)" button; selection is capped at 8 and propagated as ?sessions=…[&project=…].
  • frontend/src/lib/types.ts / api.ts: typed CompareResponse tree and a single api.compare() method using URLSearchParams per the lib/AGENTS.md rule.

Confidence Score: 5/5

Safe to merge — the change is purely additive (new page, new API method, new types, exported utilities) with no modifications to existing data paths or backend contracts.

The compare page correctly handles every edge case: missing sessions render as stub rows, the empty feature_overlap union arm is guarded with Array.isArray, sort direction defaults respect lower-is-better semantics, the Suspense boundary prevents a static-prerendering error, and the openCompare project-ID lookup uses the full unfiltered row state. MetricsTab exports are additive only, and the API client follows the URLSearchParams convention required by the lib guidelines.

No files require special attention.

Important Files Changed

Filename Overview
frontend/src/app/compare/page.tsx New 662-line compare leaderboard page: sortable table with per-metric best-value highlighting, overlaid recharts panels, session legend that toggles across all charts, and feature-overlap section. Logic is sound — missing sessions are handled as stub rows, sort direction defaults correctly for loss vs accuracy metrics, and the Suspense boundary for useSearchParams is correctly placed.
frontend/src/app/experiments/page.tsx Adds checkbox column and 'Compare selected (n/8)' affordance. Project-ID lookup in openCompare correctly uses the raw (unfiltered) rows state variable, not filteredRows, so it returns the right project scope regardless of any active search query.
frontend/src/components/MetricsTab.tsx Pure export additions (PALETTE, ChartTooltip, smartFormat, compactFormat, isLowerBetter, prettyMetricName) — no behavior change to the existing MetricsTab component.
frontend/src/lib/api.ts Adds typed api.compare() using URLSearchParams as required by lib/AGENTS.md; clean one-round-trip GET to /compare.
frontend/src/lib/types.ts Adds CompareSessionInfo, CompareMetricSeries, CompareFeatureOverlap, CompareSessionTotals, and CompareResponse types mirroring the backend schema; the feature_overlap union arm [] is typed as never[] (previously discussed in review thread).
docs/manual-tests/compare-leaderboard.md New manual-test tutorial covering all five sections of the compare flow, including edge cases (missing sessions, no metrics, direct URL load, backend unavailable).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant ExperimentsPage as /experiments
    participant ComparePage as /compare
    participant API as frontend api.ts
    participant Backend as /api/compare

    User->>ExperimentsPage: Tick checkboxes (≥2 sessions)
    ExperimentsPage->>ExperimentsPage: toggleSelected(sessionId) → selectedSessions Set
    User->>ExperimentsPage: Click "Compare selected (n/8)"
    ExperimentsPage->>ExperimentsPage: openCompare() — build URLSearchParams
    ExperimentsPage->>ComparePage: "router.push(/compare?sessions=a,b[&project=p])"

    Note over ComparePage: useSearchParams → sessionIds[]
    ComparePage->>API: api.compare(sessionIds)
    API->>Backend: "GET /api/compare?sessions=a,b"
    Backend-->>API: "CompareResponse {sessions, metrics, feature_overlap, totals}"
    API-->>ComparePage: CompareResponse

    ComparePage->>ComparePage: Build LeaderRow[] + bestPerMetric map
    ComparePage->>ComparePage: Build chart data (stepMap per metric)
    ComparePage->>User: Render leaderboard table + charts + feature overlap

    User->>ComparePage: Click column header
    ComparePage->>ComparePage: toggleSort(key) → sortedRows re-computed

    User->>ComparePage: Click session in legend
    ComparePage->>ComparePage: toggleSession(id) → hiddenSessions, charts memo recalculates
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant ExperimentsPage as /experiments
    participant ComparePage as /compare
    participant API as frontend api.ts
    participant Backend as /api/compare

    User->>ExperimentsPage: Tick checkboxes (≥2 sessions)
    ExperimentsPage->>ExperimentsPage: toggleSelected(sessionId) → selectedSessions Set
    User->>ExperimentsPage: Click "Compare selected (n/8)"
    ExperimentsPage->>ExperimentsPage: openCompare() — build URLSearchParams
    ExperimentsPage->>ComparePage: "router.push(/compare?sessions=a,b[&project=p])"

    Note over ComparePage: useSearchParams → sessionIds[]
    ComparePage->>API: api.compare(sessionIds)
    API->>Backend: "GET /api/compare?sessions=a,b"
    Backend-->>API: "CompareResponse {sessions, metrics, feature_overlap, totals}"
    API-->>ComparePage: CompareResponse

    ComparePage->>ComparePage: Build LeaderRow[] + bestPerMetric map
    ComparePage->>ComparePage: Build chart data (stepMap per metric)
    ComparePage->>User: Render leaderboard table + charts + feature overlap

    User->>ComparePage: Click column header
    ComparePage->>ComparePage: toggleSort(key) → sortedRows re-computed

    User->>ComparePage: Click session in legend
    ComparePage->>ComparePage: toggleSession(id) → hiddenSessions, charts memo recalculates
Loading

Comments Outside Diff (1)

  1. frontend/src/app/experiments/page.tsx, line 719-722 (link)

    P2 Project-ID check uses filtered rows, not all selected

    openCompare reads project_id only from the currently visible rows (which are already filtered by the search box). selectedSessions is independent state, so it can still hold sessions whose experiments have been scrolled out of view by a search filter. If a user selects two sessions from different projects, then types in the search box until only one project's rows remain visible, and then hits "Compare selected", projectIds will contain only one project ID and the compare URL will incorrectly carry project=<that id>, causing the compare header to show the wrong project badge.

    Fix: look up project IDs from the full (unfiltered) experiments list, or derive it from the raw experiments array stored in state rather than rows.

    Fix in Claude Code

Reviews (2): Last reviewed commit: "fix(review): address Greptile findings o..." | Re-trigger Greptile

Context used (3)

  • Context used - frontend/AGENTS.md (source)
  • Context used - frontend/src/components/AGENTS.md (source)
  • Context used - frontend/src/lib/AGENTS.md (source)

The backend /compare endpoint already aggregates metrics, feature
overlap, and cost totals across up to 8 sessions, but nothing in the
frontend consumed it. This adds the leaderboard UI:

- api.ts: typed api.compare(sessionIds) client fn; types.ts: Compare*
  types mirroring routers/compare.py (incl. the feature_overlap
  list-when-empty quirk)
- /compare page: sortable leaderboard table (per-metric latest values
  with best-value highlight, total cost, created-at), overlaid
  per-metric charts reusing the MetricsTab chart stack (palette,
  tooltip, formatters now exported), session legend toggles, and a
  feature-overlap panel (common + per-session extras)
- experiments page: checkbox column + "Compare selected (n/8)" header
  action that navigates to /compare with the selected session ids
  (project-scoped when the selection stays within one project)

Closes #105

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
Comment thread frontend/src/lib/types.ts Outdated
- types.ts: feature_overlap empty-list arm typed as never[] instead of
  the empty-tuple type [] — clearer intent, same Array.isArray narrowing
- add docs/manual-tests/compare-leaderboard.md (no frontend unit-test
  framework exists; manual test tutorial covers selection flow,
  leaderboard, charts, feature overlap, edge cases)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp66uSq9saSpRq9Bbmjxj4
@lucastononro

Copy link
Copy Markdown
Owner Author

Greptile review follow-up (a86a93f)

Fixed (1/1):

  • [P2] frontend/src/lib/types.ts:671 — applied the suggestion: feature_overlap: CompareFeatureOverlap | never[] instead of the empty-tuple type []. Runtime-identical (the Array.isArray narrowing in compare/page.tsx is unchanged), but never[] states "empty array" without tuple semantics.

Dismissed: none.

Tests:

  • The frontend has no unit-test framework (scripts: lint/build/format only) and the fix is type-only with no runtime surface, so instead of bootstrapping vitest for this PR I added a manual test tutorial: docs/manual-tests/compare-leaderboard.md — covers the experiments-page selection flow (incl. the 8-session cap), leaderboard sorting/trophy/lower-is-better, chart legend toggles, both feature_overlap arms (object vs empty list), and edge cases (missing session ids, single session, backend down, hard refresh).
  • Backend untouched by this follow-up, so no pytest changes.

Validation (all green): npm cinpx tsc --noEmitnpm run lint (0 warnings) → npm run build (8/8 pages, /compare 5.89 kB).

lucastononro added a commit that referenced this pull request Jul 29, 2026
Conflicts/semantic reconciliation:
- MetricsTab.tsx: #157 typed ChartTooltip props (dropped `any`); staging's
  #161 compare page imports it — kept the typed props AND the export.
- SSE union extended with events other staging PRs added/consume:
  `budget_exceeded` (#165, new BudgetExceededSSEData) and the
  `notebook.kernel.state`/`notebook.structure.changed`/`notebook.cell.*`
  events dispatched by useNotebookSSE (#148's typed bus requires them).
  StructureChangedEvent moved to lib/notebook/types.ts (re-exported from
  useNotebookSSE) so lib/types.ts can reference it.
- page.tsx budget_exceeded case adapted to #157's per-case
  `const data = event.data` block pattern.
- useNotebookSSE.test.tsx: cell.completed payload gained exec_count: null
  to satisfy the now-typed CellCompletedEvent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build the experiment/model comparison leaderboard UI

1 participant