Comparison leaderboard UI consuming the /compare endpoint - #161
Open
lucastononro wants to merge 2 commits into
Open
Comparison leaderboard UI consuming the /compare endpoint#161lucastononro wants to merge 2 commits into
lucastononro wants to merge 2 commits into
Conversation
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
- 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
Owner
Author
Greptile review follow-up (a86a93f)Fixed (1/1):
Dismissed: none. Tests:
Validation (all green): |
lucastononro
added a commit
that referenced
this pull request
Jul 29, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The backend
/compareendpoint (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.ts—CompareResponse+CompareSessionInfo/CompareMetricSeries/CompareFeatureOverlap/CompareSessionTotalstypes mirroringrouters/compare.py(including the quirk thatfeature_overlapis an empty list when no session has a prep summary).frontend/src/lib/api.ts— typedapi.compare(sessionIds)client fn (GET /api/compare?sessions=a,b,cviaURLSearchParams).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):useSearchParamswrapped 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/comparewith the selected session ids, passingproject=when the selection stays within one project. Selection is capped at the backend's 8-session limit.Test plan
Existing
MetricsTabdefault export untouched — only named exports added)./comparebackend API unchanged; no backend edits.New (click-path)
/experiments)./compare?sessions=<id1>,<id2>[&project=<id>].npx tsc --noEmit,npm run lint,npm run buildall green (/compareprerenders 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
/comparebackend endpoint, completing the feature requested in issue #105. A singleapi.compare(sessionIds)round-trip returns all data (session headers, per-metric series, feature overlap, cost totals) and the new/comparepage 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), overlaidrechartspanels with per-session color assignment fromPALETTE, a legend that toggles series across all charts simultaneously, and a feature-overlap panel that correctly disappears whenfeature_overlapis an empty list. ASuspenseboundary wraps the inner component souseSearchParamsdoesn'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: typedCompareResponsetree and a singleapi.compare()method usingURLSearchParamsper 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
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%%{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 recalculatesComments Outside Diff (1)
frontend/src/app/experiments/page.tsx, line 719-722 (link)openComparereadsproject_idonly from the currently visiblerows(which are already filtered by the search box).selectedSessionsis 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",projectIdswill contain only one project ID and the compare URL will incorrectly carryproject=<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
experimentsarray stored in state rather thanrows.Reviews (2): Last reviewed commit: "fix(review): address Greptile findings o..." | Re-trigger Greptile
Context used (3)