From a827cd1fb43c3502e22b60659964c4986f16eb4b Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 16 Mar 2026 08:42:31 -0400 Subject: [PATCH 1/3] feat: surface bright spots and exemplar sessions --- .../__tests__/bright-spot-cards.test.tsx | 49 ++++++++ .../components/growth/bright-spot-cards.tsx | 105 ++++++++++++++++++ frontend/src/pages/__tests__/growth.test.tsx | 3 + frontend/src/pages/growth.tsx | 2 + frontend/src/types/api.ts | 18 +++ src/primer/common/schemas.py | 18 +++ .../server/services/insights_service.py | 51 +++++++++ tests/test_growth.py | 41 +++++++ 8 files changed, 287 insertions(+) create mode 100644 frontend/src/components/growth/__tests__/bright-spot-cards.test.tsx create mode 100644 frontend/src/components/growth/bright-spot-cards.tsx diff --git a/frontend/src/components/growth/__tests__/bright-spot-cards.test.tsx b/frontend/src/components/growth/__tests__/bright-spot-cards.test.tsx new file mode 100644 index 00000000..2fb4b920 --- /dev/null +++ b/frontend/src/components/growth/__tests__/bright-spot-cards.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from "@testing-library/react" +import { MemoryRouter } from "react-router-dom" + +import { BrightSpotCards } from "@/components/growth/bright-spot-cards" + +describe("BrightSpotCards", () => { + it("renders an empty state", () => { + render( + + + , + ) + + expect(screen.getByText("No bright spots identified yet.")).toBeInTheDocument() + }) + + it("renders exemplar session links and metrics", () => { + render( + + + , + ) + + expect(screen.getByText("Bright spot: debugging on primer")).toBeInTheDocument() + expect(screen.getByText("Alice Example")).toBeInTheDocument() + expect(screen.getByText("View session")).toHaveAttribute("href", "/sessions/sess-1") + expect(screen.getByText("Read")).toBeInTheDocument() + expect(screen.getByText("100%")).toBeInTheDocument() + }) +}) diff --git a/frontend/src/components/growth/bright-spot-cards.tsx b/frontend/src/components/growth/bright-spot-cards.tsx new file mode 100644 index 00000000..12b27f4f --- /dev/null +++ b/frontend/src/components/growth/bright-spot-cards.tsx @@ -0,0 +1,105 @@ +import { Link } from "react-router-dom" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { formatDuration, formatPercent } from "@/lib/utils" +import type { BrightSpot } from "@/types/api" + +interface BrightSpotCardsProps { + spots: BrightSpot[] +} + +function titleize(value: string) { + return value.replaceAll("_", " ") +} + +export function BrightSpotCards({ spots }: BrightSpotCardsProps) { + if (spots.length === 0) { + return ( +
+ No bright spots identified yet. +
+ ) + } + + return ( +
+ {spots.map((spot) => ( + + +
+
+ {spot.title} +

{spot.summary}

+
+ + {titleize(spot.cluster_type)} + +
+
+ +
+
+

Success

+

{formatPercent(spot.success_rate)}

+
+
+

Sessions

+

{spot.session_count}

+
+
+

Engineers

+

{spot.engineer_count}

+
+
+

+ Avg Duration +

+

{formatDuration(spot.avg_duration)}

+
+
+ +
+

+ Exemplar Session +

+
+
+

{spot.exemplar_engineer_name}

+

+ {spot.cluster_label} + {" • "} + {formatDuration(spot.exemplar_duration_seconds)} +

+
+ + View session + +
+
+ +
+

+ Exemplar Tools +

+
+ {spot.exemplar_tools.length === 0 ? ( + No tool telemetry + ) : ( + spot.exemplar_tools.map((tool) => ( + + {tool} + + )) + )} +
+
+
+
+ ))} +
+ ) +} diff --git a/frontend/src/pages/__tests__/growth.test.tsx b/frontend/src/pages/__tests__/growth.test.tsx index 71ce7740..ffd1e2ac 100644 --- a/frontend/src/pages/__tests__/growth.test.tsx +++ b/frontend/src/pages/__tests__/growth.test.tsx @@ -34,6 +34,9 @@ vi.mock("@/components/growth/onboarding-recommendations", () => ({ vi.mock("@/components/growth/pattern-summary", () => ({ PatternSummary: () =>
pattern summary
, })) +vi.mock("@/components/growth/bright-spot-cards", () => ({ + BrightSpotCards: () =>
bright spots
, +})) vi.mock("@/components/growth/shared-pattern-card", () => ({ SharedPatternCards: () =>
shared patterns
, })) diff --git a/frontend/src/pages/growth.tsx b/frontend/src/pages/growth.tsx index 51109635..0a957c48 100644 --- a/frontend/src/pages/growth.tsx +++ b/frontend/src/pages/growth.tsx @@ -18,6 +18,7 @@ import { NewHireTable } from "@/components/growth/new-hire-table" import { VelocityChart } from "@/components/growth/velocity-chart" import { OnboardingRecommendations } from "@/components/growth/onboarding-recommendations" import { PatternSummary } from "@/components/growth/pattern-summary" +import { BrightSpotCards } from "@/components/growth/bright-spot-cards" import { SharedPatternCards } from "@/components/growth/shared-pattern-card" import { SkillInventorySummary } from "@/components/insights/skill-inventory-summary" import { CoverageSummary } from "@/components/growth/coverage-summary" @@ -67,6 +68,7 @@ function PatternsTab({ teamId, startDate, endDate }: TabProps) { return (
+
) diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index e94e825b..1182be14 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -811,8 +811,26 @@ export interface SharedPattern { insight: string } +export interface BrightSpot { + bright_spot_id: string + title: string + summary: string + cluster_type: string + cluster_label: string + session_count: number + engineer_count: number + success_rate: number | null + avg_duration: number | null + exemplar_session_id: string + exemplar_engineer_id: string + exemplar_engineer_name: string + exemplar_duration_seconds: number | null + exemplar_tools: string[] +} + export interface PatternSharingResponse { patterns: SharedPattern[] + bright_spots: BrightSpot[] total_clusters_found: number sessions_analyzed: number } diff --git a/src/primer/common/schemas.py b/src/primer/common/schemas.py index a66965e1..782c2299 100644 --- a/src/primer/common/schemas.py +++ b/src/primer/common/schemas.py @@ -1149,8 +1149,26 @@ class SharedPattern(BaseModel): insight: str +class BrightSpot(BaseModel): + bright_spot_id: str + title: str + summary: str + cluster_type: str + cluster_label: str + session_count: int + engineer_count: int + success_rate: float | None + avg_duration: float | None + exemplar_session_id: str + exemplar_engineer_id: str + exemplar_engineer_name: str + exemplar_duration_seconds: float | None + exemplar_tools: list[str] + + class PatternSharingResponse(BaseModel): patterns: list[SharedPattern] + bright_spots: list[BrightSpot] = [] total_clusters_found: int sessions_analyzed: int diff --git a/src/primer/server/services/insights_service.py b/src/primer/server/services/insights_service.py index b5489f1c..05afc69d 100644 --- a/src/primer/server/services/insights_service.py +++ b/src/primer/server/services/insights_service.py @@ -11,6 +11,7 @@ from primer.common.models import Engineer, SessionFacets, ToolUsage from primer.common.models import Session as SessionModel from primer.common.schemas import ( + BrightSpot, CohortMetrics, ConfigOptimizationResponse, ConfigSuggestion, @@ -1025,11 +1026,61 @@ def _build_pattern( return PatternSharingResponse( patterns=patterns, + bright_spots=_derive_bright_spots(patterns), total_clusters_found=len(patterns), sessions_analyzed=len(sessions), ) +def _derive_bright_spots(patterns: list[SharedPattern], limit: int = 3) -> list[BrightSpot]: + candidates = [ + pattern + for pattern in patterns + if pattern.best_approach is not None + and pattern.success_rate is not None + and pattern.success_rate >= 0.75 + and pattern.engineer_count >= 2 + and pattern.session_count >= 3 + ] + candidates.sort( + key=lambda pattern: ( + -(pattern.success_rate or 0.0), + -pattern.engineer_count, + -pattern.session_count, + pattern.avg_duration or float("inf"), + ) + ) + + bright_spots: list[BrightSpot] = [] + for pattern in candidates[:limit]: + exemplar = pattern.best_approach + if exemplar is None: + continue + bright_spots.append( + BrightSpot( + bright_spot_id=pattern.cluster_id, + title=f"Bright spot: {pattern.cluster_label}", + summary=( + f"{pattern.engineer_count} engineers converged on this pattern across " + f"{pattern.session_count} sessions. {exemplar.name}'s exemplar session is the " + "best place to copy the approach." + ), + cluster_type=pattern.cluster_type, + cluster_label=pattern.cluster_label, + session_count=pattern.session_count, + engineer_count=pattern.engineer_count, + success_rate=pattern.success_rate, + avg_duration=pattern.avg_duration, + exemplar_session_id=exemplar.session_id, + exemplar_engineer_id=exemplar.engineer_id, + exemplar_engineer_name=exemplar.name, + exemplar_duration_seconds=exemplar.duration_seconds, + exemplar_tools=exemplar.tools_used, + ) + ) + return bright_spots + + # --------------------------------------------------------------------------- # Onboarding Acceleration # --------------------------------------------------------------------------- diff --git a/tests/test_growth.py b/tests/test_growth.py index cc2be4bb..54147f53 100644 --- a/tests/test_growth.py +++ b/tests/test_growth.py @@ -279,6 +279,47 @@ def test_best_approach_selects_fastest_successful(self, client, db_session, admi assert best is not None assert best["engineer_id"] == engineers[1].id + def test_bright_spots_surface_exemplar_sessions_for_strong_patterns( + self, client, db_session, admin_headers + ): + team, engineers = _create_team_engineers(db_session, 3) + now = datetime.now(UTC) + + durations = [180.0, 60.0, 120.0] + session_ids = [] + for engineer, duration in zip(engineers, durations, strict=True): + session = _create_session( + db_session, + engineer, + started_at=now - timedelta(hours=1), + project_name="primer", + duration_seconds=duration, + ) + session_ids.append(session.id) + db_session.add( + SessionFacets(session_id=session.id, session_type="debugging", outcome="success") + ) + db_session.add(ToolUsage(session_id=session.id, tool_name="Read", call_count=5)) + db_session.add(ToolUsage(session_id=session.id, tool_name="Edit", call_count=2)) + db_session.flush() + + response = client.get( + f"/api/v1/analytics/pattern-sharing?team_id={team.id}", + headers=admin_headers, + ) + assert response.status_code == 200 + data = response.json() + + assert len(data["bright_spots"]) >= 1 + bright_spot = data["bright_spots"][0] + assert bright_spot["cluster_label"] == "debugging on primer" + assert bright_spot["session_count"] == 3 + assert bright_spot["engineer_count"] == 3 + assert bright_spot["success_rate"] == 1.0 + assert bright_spot["exemplar_engineer_id"] == engineers[1].id + assert bright_spot["exemplar_session_id"] == session_ids[1] + assert bright_spot["exemplar_tools"] == ["Edit", "Read"] + def test_best_approach_treats_legacy_success_as_success( self, client, db_session, admin_headers ): From 04bbdfe3be8604599166b628c043fae579143a77 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 16 Mar 2026 11:36:02 -0400 Subject: [PATCH 2/3] feat: add workflow compare mode --- .../__tests__/workflow-compare-card.test.tsx | 117 ++++++++++ .../src/components/finops/overview-tab.tsx | 13 +- .../finops/workflow-compare-card.tsx | 201 ++++++++++++++++++ 3 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/finops/__tests__/workflow-compare-card.test.tsx create mode 100644 frontend/src/components/finops/workflow-compare-card.tsx diff --git a/frontend/src/components/finops/__tests__/workflow-compare-card.test.tsx b/frontend/src/components/finops/__tests__/workflow-compare-card.test.tsx new file mode 100644 index 00000000..932a407f --- /dev/null +++ b/frontend/src/components/finops/__tests__/workflow-compare-card.test.tsx @@ -0,0 +1,117 @@ +import { fireEvent, render, screen } from "@testing-library/react" + +import { WorkflowCompareCard } from "@/components/finops/workflow-compare-card" + +describe("WorkflowCompareCard", () => { + it("renders an empty state when fewer than two workflows are available", () => { + render( + , + ) + + expect(screen.getByText("Need at least two workflows with cost data to compare them.")).toBeInTheDocument() + }) + + it("compares workflow metrics and allows dimension switching", () => { + render( + edit -> ship", + session_count: 1, + total_estimated_cost: 4.2, + avg_cost_per_session: 4.2, + cost_per_successful_outcome: 4.2, + }, + { + dimension: "workflow_fingerprint", + label: "refactor: search -> read -> edit -> ship", + session_count: 2, + total_estimated_cost: 5.3, + avg_cost_per_session: 2.65, + cost_per_successful_outcome: null, + }, + ]} + qualityRows={[ + { + dimension: "workflow_archetype", + label: "debugging", + linked_sessions: 2, + linked_prs: 2, + merge_rate: 1, + avg_review_comments_per_pr: 1.5, + avg_findings_per_pr: 0.5, + high_severity_findings_per_pr: 0, + avg_time_to_merge_hours: 4, + findings_fix_rate: 1, + }, + { + dimension: "workflow_archetype", + label: "refactor", + linked_sessions: 3, + linked_prs: 1, + merge_rate: 0.5, + avg_review_comments_per_pr: 2, + avg_findings_per_pr: 1, + high_severity_findings_per_pr: 0.5, + avg_time_to_merge_hours: 6, + findings_fix_rate: 0.5, + }, + { + dimension: "workflow_fingerprint", + label: "bug fix: read -> edit -> ship", + linked_sessions: 1, + linked_prs: 1, + merge_rate: 1, + avg_review_comments_per_pr: 1, + avg_findings_per_pr: 0, + high_severity_findings_per_pr: 0, + avg_time_to_merge_hours: 3, + findings_fix_rate: null, + }, + ]} + />, + ) + + expect(screen.getByText("Workflow Archetype")).toBeInTheDocument() + expect(screen.getAllByText("Debugging").length).toBeGreaterThanOrEqual(1) + expect(screen.getAllByText("Refactor").length).toBeGreaterThanOrEqual(1) + expect(screen.getAllByText("$12.50").length).toBeGreaterThanOrEqual(1) + + fireEvent.change(screen.getByLabelText("Dimension"), { + target: { value: "workflow_fingerprint" }, + }) + + expect(screen.getByText("Workflow Fingerprint")).toBeInTheDocument() + expect(screen.getAllByText("bug fix: read -> edit -> ship").length).toBeGreaterThanOrEqual(1) + expect(screen.getAllByText("refactor: search -> read -> edit -> ship").length).toBeGreaterThanOrEqual(1) + }) +}) diff --git a/frontend/src/components/finops/overview-tab.tsx b/frontend/src/components/finops/overview-tab.tsx index ac139969..57d9ad17 100644 --- a/frontend/src/components/finops/overview-tab.tsx +++ b/frontend/src/components/finops/overview-tab.tsx @@ -1,7 +1,8 @@ -import { useCostAnalytics, useProductivity } from "@/hooks/use-api-queries" +import { useCostAnalytics, useProductivity, useQualityMetrics } from "@/hooks/use-api-queries" import { DailyCostChart } from "@/components/dashboard/daily-cost-chart" import { CostBreakdownChart } from "@/components/dashboard/cost-breakdown-chart" import { WorkflowCostTable } from "@/components/finops/workflow-cost-table" +import { WorkflowCompareCard } from "@/components/finops/workflow-compare-card" import { CardSkeleton, ChartSkeleton } from "@/components/shared/loading-skeleton" import { formatCost, getModelPricing } from "@/lib/utils" @@ -14,8 +15,9 @@ interface OverviewTabProps { export function OverviewTab({ teamId, startDate, endDate }: OverviewTabProps) { const costs = useCostAnalytics(teamId, startDate, endDate) const productivity = useProductivity(teamId, startDate, endDate) + const quality = useQualityMetrics(teamId, startDate, endDate) - if (costs.isLoading || productivity.isLoading) { + if (costs.isLoading || productivity.isLoading || quality.isLoading) { return (
@@ -30,7 +32,7 @@ export function OverviewTab({ teamId, startDate, endDate }: OverviewTabProps) { ) } - if (!costs.data || !productivity.data) return null + if (!costs.data || !productivity.data || !quality.data) return null const costData = costs.data const prod = productivity.data @@ -80,6 +82,11 @@ export function OverviewTab({ teamId, startDate, endDate }: OverviewTabProps) {
+ +
) } diff --git a/frontend/src/components/finops/workflow-compare-card.tsx b/frontend/src/components/finops/workflow-compare-card.tsx new file mode 100644 index 00000000..26377039 --- /dev/null +++ b/frontend/src/components/finops/workflow-compare-card.tsx @@ -0,0 +1,201 @@ +import { useState } from "react" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { formatCost, formatLabel, formatPercent } from "@/lib/utils" +import type { QualityAttributionRow, WorkflowCostBreakdown } from "@/types/api" + +type CompareDimension = "workflow_archetype" | "workflow_fingerprint" + +interface WorkflowCompareCardProps { + workflowCosts: WorkflowCostBreakdown[] + qualityRows: QualityAttributionRow[] +} + +const DIMENSION_LABELS: Record = { + workflow_archetype: "Workflow Archetype", + workflow_fingerprint: "Workflow Fingerprint", +} + +function displayLabel(value: string, dimension: CompareDimension) { + return dimension === "workflow_archetype" ? formatLabel(value) : value +} + +function formatMetric(value: number | null | undefined, digits = 1): string { + return value == null ? "-" : value.toFixed(digits) +} + +export function WorkflowCompareCard({ + workflowCosts, + qualityRows, +}: WorkflowCompareCardProps) { + const [dimension, setDimension] = useState("workflow_archetype") + const [leftLabel, setLeftLabel] = useState("") + const [rightLabel, setRightLabel] = useState("") + + const options = workflowCosts.filter((row) => row.dimension === dimension) + const labels = options.map((row) => row.label) + const effectiveLeftLabel = labels.includes(leftLabel) ? leftLabel : (labels[0] ?? "") + const effectiveRightLabel = + labels.includes(rightLabel) && rightLabel !== effectiveLeftLabel + ? rightLabel + : (labels.find((label) => label !== effectiveLeftLabel) ?? "") + + if (options.length < 2) { + return ( + + + Workflow Compare + + +

+ Need at least two workflows with cost data to compare them. +

+
+
+ ) + } + + const costMap = Object.fromEntries( + workflowCosts + .filter((row) => row.dimension === dimension) + .map((row) => [row.label, row]), + ) + const qualityMap = Object.fromEntries( + qualityRows + .filter((row) => row.dimension === dimension) + .map((row) => [row.label, row]), + ) + + const leftCost = costMap[effectiveLeftLabel] + const rightCost = costMap[effectiveRightLabel] + const leftQuality = qualityMap[effectiveLeftLabel] + const rightQuality = qualityMap[effectiveRightLabel] + + if (!leftCost || !rightCost) { + return null + } + + return ( + + + Workflow Compare + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metric{displayLabel(effectiveLeftLabel, dimension)}{displayLabel(effectiveRightLabel, dimension)}
Sessions{leftCost.session_count}{rightCost.session_count}
Total Cost{formatCost(leftCost.total_estimated_cost)}{formatCost(rightCost.total_estimated_cost)}
Avg Cost / Session + {leftCost.avg_cost_per_session != null ? formatCost(leftCost.avg_cost_per_session) : "-"} + + {rightCost.avg_cost_per_session != null ? formatCost(rightCost.avg_cost_per_session) : "-"} +
Cost / Success + {leftCost.cost_per_successful_outcome != null + ? formatCost(leftCost.cost_per_successful_outcome) + : "-"} + + {rightCost.cost_per_successful_outcome != null + ? formatCost(rightCost.cost_per_successful_outcome) + : "-"} +
Linked PRs{leftQuality?.linked_prs ?? "-"}{rightQuality?.linked_prs ?? "-"}
Merge Rate{formatPercent(leftQuality?.merge_rate)}{formatPercent(rightQuality?.merge_rate)}
Reviews / PR{formatMetric(leftQuality?.avg_review_comments_per_pr)}{formatMetric(rightQuality?.avg_review_comments_per_pr)}
Findings / PR{formatMetric(leftQuality?.avg_findings_per_pr)}{formatMetric(rightQuality?.avg_findings_per_pr)}
Merge Time (h){formatMetric(leftQuality?.avg_time_to_merge_hours)}{formatMetric(rightQuality?.avg_time_to_merge_hours)}
+
+
+
+ ) +} From 2722a5f987ebb7295e16163ebf5f3251ba447522 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Mar 2026 15:50:07 +0000 Subject: [PATCH 3/3] Fix: show dimension selector in empty state, decouple quality query from overview tab, deduplicate titleize utility - workflow-compare-card: always render dimension selector even when current dimension has fewer than 2 options, so users can switch dimensions - overview-tab: remove quality query from hard loading/data gates so the tab renders cost/productivity data even if quality fetch fails - Extract titleize() into @/lib/utils and import it in all 4 consumer files instead of duplicating the function definition --- .../bottlenecks/root-cause-cluster-list.tsx | 6 +--- .../src/components/finops/overview-tab.tsx | 6 ++-- .../finops/workflow-compare-card.tsx | 29 +++++++++++-------- .../components/growth/bright-spot-cards.tsx | 6 +--- .../growth/workflow-playbook-cards.tsx | 6 +--- .../projects/project-workflow-section.tsx | 6 +--- frontend/src/lib/utils.ts | 4 +++ 7 files changed, 28 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/bottlenecks/root-cause-cluster-list.tsx b/frontend/src/components/bottlenecks/root-cause-cluster-list.tsx index 0df2d3a7..7129021a 100644 --- a/frontend/src/components/bottlenecks/root-cause-cluster-list.tsx +++ b/frontend/src/components/bottlenecks/root-cause-cluster-list.tsx @@ -1,16 +1,12 @@ import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { formatPercent } from "@/lib/utils" +import { formatPercent, titleize } from "@/lib/utils" import type { RootCauseCluster } from "@/types/api" interface RootCauseClusterListProps { data: RootCauseCluster[] } -function titleize(value: string) { - return value.replaceAll("_", " ") -} - export function RootCauseClusterList({ data }: RootCauseClusterListProps) { if (data.length === 0) return null diff --git a/frontend/src/components/finops/overview-tab.tsx b/frontend/src/components/finops/overview-tab.tsx index 57d9ad17..fc7eee98 100644 --- a/frontend/src/components/finops/overview-tab.tsx +++ b/frontend/src/components/finops/overview-tab.tsx @@ -17,7 +17,7 @@ export function OverviewTab({ teamId, startDate, endDate }: OverviewTabProps) { const productivity = useProductivity(teamId, startDate, endDate) const quality = useQualityMetrics(teamId, startDate, endDate) - if (costs.isLoading || productivity.isLoading || quality.isLoading) { + if (costs.isLoading || productivity.isLoading) { return (
@@ -32,7 +32,7 @@ export function OverviewTab({ teamId, startDate, endDate }: OverviewTabProps) { ) } - if (!costs.data || !productivity.data || !quality.data) return null + if (!costs.data || !productivity.data) return null const costData = costs.data const prod = productivity.data @@ -85,7 +85,7 @@ export function OverviewTab({ teamId, startDate, endDate }: OverviewTabProps) {
) diff --git a/frontend/src/components/finops/workflow-compare-card.tsx b/frontend/src/components/finops/workflow-compare-card.tsx index 26377039..196cf02c 100644 --- a/frontend/src/components/finops/workflow-compare-card.tsx +++ b/frontend/src/components/finops/workflow-compare-card.tsx @@ -40,13 +40,28 @@ export function WorkflowCompareCard({ ? rightLabel : (labels.find((label) => label !== effectiveLeftLabel) ?? "") + const dimensionSelector = ( + + ) + if (options.length < 2) { return ( Workflow Compare - + +
{dimensionSelector}

Need at least two workflows with cost data to compare them.

@@ -82,17 +97,7 @@ export function WorkflowCompareCard({
- + {dimensionSelector}