From bce1585ca34cf0698311ec2f001f204f31281842 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 16 Mar 2026 11:36:02 -0400 Subject: [PATCH 1/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 0000000..932a407 --- /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 ac13996..57d9ad1 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 0000000..2637703 --- /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 cf9a279c55446ab1390945cfd779e5fc85d19c1a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 16 Mar 2026 17:56:44 +0000 Subject: [PATCH 2/3] Centralize formatMetric utility in utils.ts to eliminate duplication --- .../src/components/finops/workflow-compare-card.tsx | 6 +----- .../src/components/quality/claude-pr-comparison.tsx | 12 ++++++------ .../components/quality/quality-attribution-table.tsx | 6 +----- frontend/src/lib/utils.ts | 4 ++++ 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/finops/workflow-compare-card.tsx b/frontend/src/components/finops/workflow-compare-card.tsx index 2637703..17d295f 100644 --- a/frontend/src/components/finops/workflow-compare-card.tsx +++ b/frontend/src/components/finops/workflow-compare-card.tsx @@ -1,7 +1,7 @@ import { useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { formatCost, formatLabel, formatPercent } from "@/lib/utils" +import { formatCost, formatLabel, formatMetric, formatPercent } from "@/lib/utils" import type { QualityAttributionRow, WorkflowCostBreakdown } from "@/types/api" type CompareDimension = "workflow_archetype" | "workflow_fingerprint" @@ -20,10 +20,6 @@ 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, diff --git a/frontend/src/components/quality/claude-pr-comparison.tsx b/frontend/src/components/quality/claude-pr-comparison.tsx index 4d53336..c440b8a 100644 --- a/frontend/src/components/quality/claude-pr-comparison.tsx +++ b/frontend/src/components/quality/claude-pr-comparison.tsx @@ -1,7 +1,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { CardSkeleton } from "@/components/shared/loading-skeleton" import { useClaudePRComparison } from "@/hooks/use-api-queries" -import { cn } from "@/lib/utils" +import { cn, formatMetric } from "@/lib/utils" import { GitPullRequest } from "lucide-react" import type { PRGroupMetrics } from "@/types/api" @@ -11,9 +11,9 @@ interface ClaudePRComparisonProps { endDate?: string } -function formatMetric(value: number | null, suffix = ""): string { - if (value == null) return "-" - return `${value.toFixed(1)}${suffix}` +function formatMetricWithSuffix(value: number | null, suffix = ""): string { + const base = formatMetric(value) + return base === "-" ? base : `${base}${suffix}` } function DeltaIndicator({ value }: { value: number | null }) { @@ -56,8 +56,8 @@ function MetricRow({ return (
{label}
-
{formatMetric(claudeValue, suffix)}
-
{formatMetric(otherValue, suffix)}
+
{formatMetricWithSuffix(claudeValue, suffix)}
+
{formatMetricWithSuffix(otherValue, suffix)}
diff --git a/frontend/src/components/quality/quality-attribution-table.tsx b/frontend/src/components/quality/quality-attribution-table.tsx index 1db6c30..e490cd5 100644 --- a/frontend/src/components/quality/quality-attribution-table.tsx +++ b/frontend/src/components/quality/quality-attribution-table.tsx @@ -1,4 +1,4 @@ -import { formatLabel } from "@/lib/utils" +import { formatLabel, formatMetric } from "@/lib/utils" import type { QualityAttributionRow } from "@/types/api" interface Props { @@ -18,10 +18,6 @@ function formatPercent(value: number | null): string { return value == null ? "-" : `${(value * 100).toFixed(0)}%` } -function formatMetric(value: number | null, digits = 1): string { - return value == null ? "-" : value.toFixed(digits) -} - export function QualityAttributionTable({ rows }: Props) { if (rows.length === 0) { return ( diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 5a23952..ba7c603 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -33,6 +33,10 @@ export function formatLabel(value: string): string { .replace(/\b\w/g, (char) => char.toUpperCase()) } +export function formatMetric(value: number | null | undefined, digits = 1): string { + return value == null ? "-" : value.toFixed(digits) +} + export function formatPercent(value: number | null | undefined): string { if (value == null) return "-" return `${(value * 100).toFixed(0)}%` From 142689f0077d8e71abdbf3791a2276ff6c2a07ee Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 16 Mar 2026 14:12:07 -0400 Subject: [PATCH 3/3] fix: address workflow compare mode bugbot feedback --- .../bottlenecks/root-cause-cluster-list.tsx | 6 +-- .../src/components/finops/overview-tab.tsx | 6 +-- .../finops/workflow-compare-card.tsx | 46 ++++++++++++------- .../components/growth/bright-spot-cards.tsx | 4 +- .../growth/workflow-playbook-cards.tsx | 6 +-- .../projects/project-workflow-section.tsx | 6 +-- frontend/src/lib/utils.ts | 4 ++ 7 files changed, 42 insertions(+), 36 deletions(-) diff --git a/frontend/src/components/bottlenecks/root-cause-cluster-list.tsx b/frontend/src/components/bottlenecks/root-cause-cluster-list.tsx index 0df2d3a..7129021 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 57d9ad1..fc7eee9 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 17d295f..593f109 100644 --- a/frontend/src/components/finops/workflow-compare-card.tsx +++ b/frontend/src/components/finops/workflow-compare-card.tsx @@ -36,13 +36,29 @@ 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.

@@ -78,17 +94,7 @@ export function WorkflowCompareCard({
- + {dimensionSelector}
- {formatLabel(spot.cluster_type)} + {titleize(spot.cluster_type)}
diff --git a/frontend/src/components/growth/workflow-playbook-cards.tsx b/frontend/src/components/growth/workflow-playbook-cards.tsx index 8a7d8b6..4e7e8c1 100644 --- a/frontend/src/components/growth/workflow-playbook-cards.tsx +++ b/frontend/src/components/growth/workflow-playbook-cards.tsx @@ -1,6 +1,6 @@ import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { formatDuration, formatPercent } from "@/lib/utils" +import { formatDuration, formatPercent, titleize } from "@/lib/utils" import type { WorkflowPlaybook } from "@/types/api" interface WorkflowPlaybookCardsProps { @@ -13,10 +13,6 @@ const adoptionVariant: Record = { already_using: "outline", } -function titleize(value: string) { - return value.replaceAll("_", " ") -} - export function WorkflowPlaybookCards({ playbooks }: WorkflowPlaybookCardsProps) { if (playbooks.length === 0) { return ( diff --git a/frontend/src/components/projects/project-workflow-section.tsx b/frontend/src/components/projects/project-workflow-section.tsx index 5903ea3..9695491 100644 --- a/frontend/src/components/projects/project-workflow-section.tsx +++ b/frontend/src/components/projects/project-workflow-section.tsx @@ -1,16 +1,12 @@ import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { formatDuration, formatNumber, formatPercent } from "@/lib/utils" +import { formatDuration, formatNumber, formatPercent, titleize } from "@/lib/utils" import type { ProjectWorkflowSummary } from "@/types/api" interface ProjectWorkflowSectionProps { workflowSummary: ProjectWorkflowSummary } -function titleize(value: string) { - return value.replaceAll("_", " ") -} - export function ProjectWorkflowSection({ workflowSummary }: ProjectWorkflowSectionProps) { return (
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index ba7c603..45a20de 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -27,6 +27,10 @@ export function formatCost(dollars: number): string { return `$${dollars.toFixed(2)}` } +export function titleize(value: string): string { + return value.replaceAll("_", " ") +} + export function formatLabel(value: string): string { return value .replaceAll("_", " ")