Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,14 @@ As usage grows, the platform needs stronger derived data pipelines, performance
- [x] Conversational data explorer (SSE-streamed tool-use chat)
- [x] AI-powered recommendations panel
- [ ] [P1] Saved explorer prompts and reusable report cards
- [ ] [P1] Compare mode for engineer, team, project, and time-period analysis
- [x] [P1] Compare mode for engineer, team, project, and time-period analysis
- [ ] [P2] Weekly manager review packs that combine quality, friction, growth, and cost
- [ ] [P2] Recommendation narratives that explain why a workflow is likely to help

## Website & Positioning

- [ ] [P1] Reposition the website around workflow intelligence for agentic engineering
- [ ] [P1] Showcase workflow cost, quality, compare mode, and exemplar sessions as the core proof points
- [x] [P1] Reposition the website around workflow intelligence for agentic engineering
- [x] [P1] Showcase workflow cost, quality, compare mode, and exemplar sessions as the core proof points

## Interventions & Experimentation

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { GrowthPage } from "@/pages/growth"
import { InterventionsPage } from "@/pages/interventions"
import { ProjectsPage } from "@/pages/projects"
import { ProjectWorkspacePage } from "@/pages/project-workspace"
import { ComparePage } from "@/pages/compare"
import { NotFoundPage } from "@/pages/not-found"
import { FloatingExplorer } from "@/components/explorer/floating-explorer"
import type { DateRange } from "@/components/layout/date-range-picker"
Expand Down Expand Up @@ -107,6 +108,7 @@ function AuthenticatedApp() {
<Route path="/friction" element={<FrictionPage teamId={teamId} dateRange={dateRange} />} />
<Route path="/interventions" element={<InterventionsPage teamId={teamId} />} />
<Route path="/growth" element={<GrowthPage teamId={teamId} dateRange={dateRange} />} />
<Route path="/compare" element={<ComparePage teamId={teamId} dateRange={dateRange} />} />
<Route path="/projects" element={<ProjectsPage teamId={teamId} dateRange={dateRange} />} />
<Route path="/projects/:projectName" element={<ProjectWorkspaceRoute teamId={teamId} dateRange={dateRange} />} />

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Sun,
Moon,
PanelLeft,
Scale,
} from "lucide-react"
import { cn } from "@/lib/utils"
import { useAuth } from "@/lib/auth-context"
Expand Down Expand Up @@ -53,6 +54,7 @@ const leadershipLinks: NavItem[] = [
{ to: "/interventions", label: "Interventions", icon: ClipboardList },
{ to: "/projects", label: "Projects", icon: FolderKanban },
{ to: "/growth", label: "Growth", icon: GraduationCap },
{ to: "/compare", label: "Compare", icon: Scale },
{ to: "/explorer", label: "Explorer", icon: MessageSquare },
{ to: "/sessions", label: "Sessions", icon: MonitorDot },
{ to: "/teams", label: "Teams", icon: Users2 },
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/hooks/use-api-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
BudgetStatus,
CacheAnalyticsResponse,
ClaudePRComparisonResponse,
CompareResponse,
ConfigOptimizationResponse,
CostAnalytics,
CrossProjectComparisonResponse,
Expand Down Expand Up @@ -197,6 +198,33 @@ export function useInterventionEffectiveness(params: {
})
}

export function useCompareAnalytics(params: {
mode: string
leftKey?: string
rightKey?: string
teamId?: string | null
startDate?: string
endDate?: string
enabled?: boolean
}) {
const { enabled = true, ...queryParams } = params
return useQuery({
queryKey: ["compare-analytics", queryParams],
queryFn: () =>
apiFetch<CompareResponse>(
`/api/v1/analytics/compare${buildParams({
mode: params.mode,
left_key: params.leftKey,
right_key: params.rightKey,
team_id: params.teamId,
start_date: params.startDate,
end_date: params.endDate,
})}`,
),
enabled,
})
}

export function useCostAnalytics(teamId: string | null, startDate?: string, endDate?: string) {
return useQuery({
queryKey: ["costs", teamId, startDate, endDate],
Expand Down
114 changes: 114 additions & 0 deletions frontend/src/pages/__tests__/compare.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { fireEvent, render, screen } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"

vi.mock("@/hooks/use-api-queries", () => ({
useTeams: vi.fn(),
useEngineers: vi.fn(),
useProjectAnalytics: vi.fn(),
useCompareAnalytics: vi.fn(),
}))

import {
useCompareAnalytics,
useEngineers,
useProjectAnalytics,
useTeams,
} from "@/hooks/use-api-queries"
import { ComparePage } from "../compare"

const mockUseTeams = vi.mocked(useTeams)
const mockUseEngineers = vi.mocked(useEngineers)
const mockUseProjectAnalytics = vi.mocked(useProjectAnalytics)
const mockUseCompareAnalytics = vi.mocked(useCompareAnalytics)

describe("ComparePage", () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseTeams.mockReturnValue({
data: [
{ id: "team-1", name: "Alpha" },
{ id: "team-2", name: "Beta" },
],
isLoading: false,
} as never)
mockUseEngineers.mockReturnValue({
data: [
{ id: "eng-1", name: "Alice", display_name: "Alice", team_id: "team-1" },
{ id: "eng-2", name: "Bob", display_name: "Bob", team_id: "team-2" },
],
isLoading: false,
} as never)
mockUseProjectAnalytics.mockReturnValue({
data: {
total_count: 2,
projects: [
{ project_name: "alpha-app" },
{ project_name: "beta-app" },
],
},
isLoading: false,
} as never)
mockUseCompareAnalytics.mockReturnValue({
data: {
mode: "team",
left: {
label: "Alpha",
total_sessions: 12,
success_rate: 0.75,
total_cost: 12.4,
avg_cost_per_session: 1.03,
cost_per_successful_outcome: 1.4,
pr_merge_rate: 0.8,
findings_fix_rate: 0.7,
effectiveness_score: 82.1,
leverage_score: 74.6,
top_workflows: [
{ label: "debugging", session_count: 5, share_of_sessions: 0.42 },
],
},
right: {
label: "Beta",
total_sessions: 8,
success_rate: 0.5,
total_cost: 10.4,
avg_cost_per_session: 1.3,
cost_per_successful_outcome: 2.1,
pr_merge_rate: 0.55,
findings_fix_rate: 0.5,
effectiveness_score: 61.4,
leverage_score: 58.2,
top_workflows: [
{ label: "implementation", session_count: 3, share_of_sessions: 0.38 },
],
},
delta: {
total_sessions: 4,
success_rate: 0.25,
total_cost: 2.0,
avg_cost_per_session: -0.27,
cost_per_successful_outcome: -0.7,
pr_merge_rate: 0.25,
findings_fix_rate: 0.2,
effectiveness_score: 20.7,
leverage_score: 16.4,
},
},
isLoading: false,
} as never)
})

it("renders compare mode snapshots and supports switching modes", () => {
render(<ComparePage teamId={null} dateRange={null} />)

expect(screen.getByText("Compare")).toBeInTheDocument()
expect(
screen.queryByText("Selected period vs previous period"),
).not.toBeInTheDocument()
expect(screen.getAllByText("Alpha").length).toBeGreaterThan(0)
expect(screen.getAllByText("Beta").length).toBeGreaterThan(0)
expect(screen.getAllByText("Top Workflows").length).toBeGreaterThan(0)

fireEvent.click(screen.getByRole("button", { name: "Periods" }))
expect(screen.getByText("Selected period vs previous period")).toBeInTheDocument()
})
})
Loading
Loading