Skip to content

Commit 0847290

Browse files
authored
Merge pull request #22 from github/feat/aic-breakdowns-and-ux
feat: expand AIC comparison breakdowns
2 parents 9690c37 + 436195d commit 0847290

12 files changed

Lines changed: 771 additions & 128 deletions

src/App.css

Lines changed: 137 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -270,15 +270,14 @@
270270
.orgs-view {
271271
display: flex;
272272
flex-direction: column;
273-
gap: 12px;
273+
gap: 20px;
274274
}
275275

276-
.orgs-view__toolbar {
276+
.orgs-view__header {
277277
display: flex;
278-
align-items: flex-end;
279-
justify-content: space-between;
280-
gap: 16px;
281-
flex-wrap: wrap;
278+
flex-direction: column;
279+
gap: 12px;
280+
margin-bottom: 8px;
282281
}
283282

284283
.orgs-view__title {
@@ -289,16 +288,101 @@
289288

290289
.orgs-view__heading {
291290
margin: 0;
292-
font-size: 18px;
291+
font-size: 24px;
292+
font-weight: 700;
293293
color: #1f2328;
294294
}
295295

296296
.orgs-view__count {
297+
font-size: 14px;
298+
font-weight: 500;
299+
color: #636c76;
300+
}
301+
302+
.orgs-view__select {
303+
border: 1px solid #d1d9e0;
304+
border-radius: 6px;
305+
padding: 10px 12px;
306+
font-size: 14px;
307+
max-width: 500px;
308+
color: #1f2328;
309+
background: #ffffff;
310+
}
311+
312+
.orgs-view__select:focus-visible {
313+
outline: 2px solid #6366f1;
314+
outline-offset: 2px;
315+
}
316+
317+
.orgs-view__tables {
318+
display: grid;
319+
grid-template-columns: 1fr;
320+
gap: 20px;
321+
}
322+
323+
.orgs-view__tableControls {
324+
display: flex;
325+
gap: 0;
326+
flex-wrap: wrap;
327+
border-bottom: 1px solid #d1d9e0;
328+
}
329+
330+
.orgs-view__tableButton {
331+
border: none;
332+
border-bottom: 2px solid transparent;
333+
margin-bottom: -1px;
334+
border-radius: 0;
335+
background: transparent;
336+
color: #636c76;
337+
padding: 8px 16px;
338+
font: inherit;
339+
font-size: 14px;
340+
font-weight: 400;
341+
cursor: pointer;
342+
transition: color 0.1s ease, border-color 0.1s ease;
343+
}
344+
345+
.orgs-view__tableButton:hover {
346+
color: #1f2328;
347+
background: transparent;
348+
}
349+
350+
.orgs-view__tableButton:focus-visible {
351+
outline: 2px solid #0969da;
352+
outline-offset: -2px;
353+
border-radius: 3px;
354+
}
355+
356+
.orgs-view__tableButton--active {
357+
color: #1f2328;
358+
font-weight: 600;
359+
border-bottom-color: #fd8c73;
360+
background: transparent;
361+
}
362+
363+
.orgs-view__tableWrap {
364+
overflow: hidden;
365+
}
366+
367+
.orgs-view__tableTitle {
368+
padding: 12px 16px;
369+
border-bottom: 1px solid #d1d9e0;
370+
font-size: 12px;
371+
font-weight: 700;
372+
letter-spacing: 0.05em;
373+
text-transform: uppercase;
374+
color: #636c76;
375+
background: #f6f8fa;
376+
}
377+
378+
.orgs-view__tableHint {
379+
padding: 12px 16px 0;
297380
font-size: 13px;
298381
color: #636c76;
299382
}
300383

301-
.orgs-view__table th.orgs-view__num {
384+
.orgs-view__table th.orgs-view__num,
385+
td.orgs-view__num {
302386
text-align: right;
303387
}
304388

@@ -322,16 +406,12 @@
322406
font-weight: 600;
323407
}
324408

325-
.orgs-view__diff--neutral {
326-
color: #8b949e;
327-
font-weight: 600;
328-
}
329-
330409
.orgs-view__empty {
331410
background: #ffffff;
332411
border: 1px solid #d1d9e0;
333412
border-radius: 6px;
334-
padding: 16px;
413+
padding: 24px;
414+
text-align: center;
335415
color: #636c76;
336416
}
337417

@@ -1387,6 +1467,49 @@ td.cost-centers-view__num {
13871467
color: #1f2328;
13881468
}
13891469

1470+
.users-view__pagination {
1471+
display: flex;
1472+
align-items: center;
1473+
justify-content: center;
1474+
gap: 16px;
1475+
padding: 12px 0;
1476+
}
1477+
1478+
.users-view__pageBtn {
1479+
border: 1px solid #d1d9e0;
1480+
border-radius: 6px;
1481+
background: #ffffff;
1482+
color: #1f2328;
1483+
padding: 6px 14px;
1484+
font: inherit;
1485+
font-size: 14px;
1486+
cursor: pointer;
1487+
transition: background 0.1s ease, border-color 0.1s ease;
1488+
}
1489+
1490+
.users-view__pageBtn:hover:not(:disabled) {
1491+
background: #f3f4f6;
1492+
border-color: #b0b7c0;
1493+
}
1494+
1495+
.users-view__pageBtn:disabled {
1496+
opacity: 0.4;
1497+
cursor: default;
1498+
}
1499+
1500+
.users-view__pageBtn:focus-visible {
1501+
outline: 2px solid #0969da;
1502+
outline-offset: 2px;
1503+
}
1504+
1505+
.users-view__pageInfo {
1506+
font-size: 13px;
1507+
color: #636c76;
1508+
font-variant-numeric: tabular-nums;
1509+
min-width: 140px;
1510+
text-align: center;
1511+
}
1512+
13901513
/* User details view */
13911514
.user-details {
13921515
display: flex;

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ function App() {
387387
</div>
388388
) : (
389389
<div className="view-content">
390-
<OrganizationsView data={orgs ?? { organizations: [] }} />
390+
<OrganizationsView data={orgs ?? { organizations: [] }} rangeStart={rangeStart} />
391391
</div>
392392
)}
393393
</main>

src/pipeline/aggregators/aicDiscountAggregation.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ describe('derived AIC discount aggregation', () => {
6464
const organization = result.organizations.organizations[0]
6565

6666
expect(organization.organization).toBe('octo')
67-
expect(organization.aicGrossAmount).toBeCloseTo(1.5)
68-
expect(organization.aicNetAmount).toBeCloseTo(0.6)
69-
expect(organization.aicDiscountAmount).toBeCloseTo(0.9)
67+
expect(organization.totals.aicGrossAmount).toBeCloseTo(1.5)
68+
expect(organization.totals.aicNetAmount).toBeCloseTo(0.6)
69+
expect(calculateAicDiscountAmount(organization.totals.aicGrossAmount, organization.totals.aicNetAmount)).toBeCloseTo(0.9)
7070
})
7171

7272
it('derives cost-center AIC discount from totals gross minus net', () => {
@@ -87,8 +87,8 @@ describe('derived AIC discount aggregation', () => {
8787
const dailyNet = sum(result.daily.dailyData.map((day) => day.aicNetAmount))
8888
const userGross = sum(result.users.users.map((user) => user.totals.aicGrossAmount))
8989
const userNet = sum(result.users.users.map((user) => user.totals.aicNetAmount))
90-
const organizationGross = sum(result.organizations.organizations.map((organization) => organization.aicGrossAmount))
91-
const organizationNet = sum(result.organizations.organizations.map((organization) => organization.aicNetAmount))
90+
const organizationGross = sum(result.organizations.organizations.map((organization) => organization.totals.aicGrossAmount))
91+
const organizationNet = sum(result.organizations.organizations.map((organization) => organization.totals.aicNetAmount))
9292
const costCenterGross = sum(result.costCenters.costCenters.map((costCenter) => costCenter.totals.aicGrossAmount))
9393
const costCenterNet = sum(result.costCenters.costCenters.map((costCenter) => costCenter.totals.aicNetAmount))
9494
const modelGross = sum(Object.values(result.models.totalsByModel).map((model) => model.aicGrossAmount))
@@ -122,6 +122,6 @@ describe('derived AIC discount aggregation', () => {
122122
])
123123
const organization = result.organizations.organizations[0]
124124

125-
expect(calculateSavingsDifference(organization.netAmount, organization.aicNetAmount)).toBeCloseTo(2.75)
125+
expect(calculateSavingsDifference(organization.totals.netAmount, organization.totals.aicNetAmount)).toBeCloseTo(2.75)
126126
})
127127
})

src/pipeline/aggregators/costCenterAggregator.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Aggregator } from './base'
22
import { getUsageMetrics, type TokenUsageHeader, type TokenUsageRecord } from '../parser'
3+
import { pickTopEntries } from './topBreakdown'
34

45
export type CostTotals = {
56
requests: number
@@ -130,16 +131,6 @@ export class CostCenterAggregator implements Aggregator<TokenUsageRecord, CostCe
130131
result(): CostCenterResult {
131132
const costCenters = Array.from(this.byCostCenter.values())
132133
.map<CostCenterUsage>((costCenter) => {
133-
const totalsByModel: Record<string, CostTotals> = {}
134-
for (const [model, totals] of costCenter.totalsByModel.entries()) {
135-
totalsByModel[model] = totals
136-
}
137-
138-
const totalsByUser: Record<string, CostCenterUserTotals> = {}
139-
for (const [user, totals] of costCenter.totalsByUser.entries()) {
140-
totalsByUser[user] = totals
141-
}
142-
143134
const userCount = costCenter.users.size
144135
const netCostPerUser = userCount > 0 ? costCenter.totals.netAmount / userCount : 0
145136

@@ -148,8 +139,8 @@ export class CostCenterAggregator implements Aggregator<TokenUsageRecord, CostCe
148139
userCount,
149140
netCostPerUser,
150141
totals: costCenter.totals,
151-
totalsByModel,
152-
totalsByUser,
142+
totalsByModel: pickTopEntries(costCenter.totalsByModel.entries()),
143+
totalsByUser: pickTopEntries(costCenter.totalsByUser.entries()),
153144
}
154145
})
155146
.sort((a, b) => a.costCenterName.localeCompare(b.costCenterName))

0 commit comments

Comments
 (0)