Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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