Skip to content

Commit 9994993

Browse files
ofershapcursoragent
andcommitted
feat: clickable user counts in model cost table to filter dashboard
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9d64566 commit 9994993

2 files changed

Lines changed: 82 additions & 10 deletions

File tree

src/app/dashboard-client.tsx

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface ModelCost {
2525
avg_spend: number;
2626
total_spend: number;
2727
total_reqs: number;
28+
emails: string[];
2829
}
2930

3031
const TIME_RANGES = [
@@ -75,6 +76,10 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
7576
label: string;
7677
emails: Set<string>;
7778
} | null>(null);
79+
const [modelFilter, setModelFilter] = useState<{
80+
model: string;
81+
emails: Set<string>;
82+
} | null>(null);
7883

7984
const groupParam = searchParams.get("group");
8085
const exhaustionParam = searchParams.get("exhaustion");
@@ -200,6 +205,9 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
200205
if (exhaustionFilter) {
201206
users = users.filter((u) => exhaustionFilter.emails.has(u.email));
202207
}
208+
if (modelFilter) {
209+
users = users.filter((u) => modelFilter.emails.has(u.email));
210+
}
203211
if (search.trim()) {
204212
const q = search.toLowerCase();
205213
users = users.filter(
@@ -240,7 +248,16 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
240248
}
241249
});
242250
return sorted;
243-
}, [stats.rankedUsers, search, sortCol, sortAsc, groupEmailSet, badgeFilter, exhaustionFilter]);
251+
}, [
252+
stats.rankedUsers,
253+
search,
254+
sortCol,
255+
sortAsc,
256+
groupEmailSet,
257+
badgeFilter,
258+
exhaustionFilter,
259+
modelFilter,
260+
]);
244261

245262
const searchedUser = useMemo(() => {
246263
if (!search.trim()) return null;
@@ -270,7 +287,8 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
270287
search.trim().length > 0 ||
271288
selectedGroup !== "all" ||
272289
badgeFilter !== null ||
273-
exhaustionFilter !== null;
290+
exhaustionFilter !== null ||
291+
modelFilter !== null;
274292
const totalLines = stats.dailyTeamActivity.reduce((s, d) => s + d.total_lines_added, 0);
275293
const effectiveDays = Math.min(days, stats.cycleDays);
276294
const cycleStartDate = new Date(stats.cycleStart);
@@ -409,6 +427,17 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
409427
</button>
410428
</span>
411429
)}
430+
{modelFilter && (
431+
<span className="inline-flex items-center gap-1.5 bg-purple-600/20 text-purple-300 rounded-md px-2 py-1 text-[11px] font-medium">
432+
Model: {shortModel(modelFilter.model)} ({modelFilter.emails.size})
433+
<button
434+
onClick={() => setModelFilter(null)}
435+
className="hover:text-purple-100 cursor-pointer"
436+
>
437+
438+
</button>
439+
</span>
440+
)}
412441
<div className="ml-auto text-[11px] text-zinc-600">
413442
{isSearching ? `${filteredUsers.length} / ` : ""}
414443
{stats.totalMembers} members
@@ -501,7 +530,15 @@ export function DashboardClient({ initialData }: DashboardClientProps) {
501530
/>
502531
</ExpandableCard>
503532
<ExpandableCard>
504-
<ModelCostComparison data={data.modelCosts} />
533+
<ModelCostComparison
534+
data={data.modelCosts}
535+
activeModel={modelFilter?.model ?? null}
536+
onModelFilter={(model, emails) => {
537+
setModelFilter((prev) =>
538+
prev?.model === model ? null : { model, emails: new Set(emails) },
539+
);
540+
}}
541+
/>
505542
</ExpandableCard>
506543
</div>
507544

@@ -604,7 +641,15 @@ function buildDailySpendData(breakdown: SpendBreakdownRow[]): {
604641
};
605642
}
606643

607-
function ModelCostComparison({ data }: { data: ModelCost[] }) {
644+
function ModelCostComparison({
645+
data,
646+
activeModel,
647+
onModelFilter,
648+
}: {
649+
data: ModelCost[];
650+
activeModel: string | null;
651+
onModelFilter: (model: string, emails: string[]) => void;
652+
}) {
608653
const rows = data
609654
.filter((d) => d.total_reqs > 0)
610655
.map((d) => ({
@@ -651,7 +696,10 @@ function ModelCostComparison({ data }: { data: ModelCost[] }) {
651696
const mult = cheapest > 0 ? row.costPerReq / cheapest : 1;
652697
const barPct = maxCostPerReq > 0 ? (row.costPerReq / maxCostPerReq) * 100 : 0;
653698
return (
654-
<tr key={row.model} className="border-b border-zinc-800/30 hover:bg-zinc-800/30">
699+
<tr
700+
key={row.model}
701+
className={`border-b border-zinc-800/30 hover:bg-zinc-800/30 ${activeModel === row.model ? "bg-purple-500/10" : ""}`}
702+
>
655703
<td className="py-1 text-zinc-300 font-mono cursor-default" title={row.model}>
656704
{shortModel(row.model)}
657705
</td>
@@ -671,7 +719,15 @@ function ModelCostComparison({ data }: { data: ModelCost[] }) {
671719
</span>
672720
</div>
673721
</td>
674-
<td className="text-right py-1 text-zinc-400">{row.users}</td>
722+
<td className="text-right py-1">
723+
<button
724+
onClick={() => onModelFilter(row.model, row.emails)}
725+
className={`font-mono cursor-pointer hover:text-purple-300 transition-colors ${activeModel === row.model ? "text-purple-400 font-bold" : "text-zinc-400"}`}
726+
title={`Filter by ${shortModel(row.model)} users`}
727+
>
728+
{row.users}
729+
</button>
730+
</td>
675731
<td className="text-right py-1 font-mono text-zinc-400">
676732
${row.total_spend.toLocaleString()}
677733
</td>

src/lib/db.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,7 @@ export interface FullDashboard {
899899
avg_spend: number;
900900
total_spend: number;
901901
total_reqs: number;
902+
emails: string[];
902903
}>;
903904
teamDailySpend: Array<{ date: string; spend_cents: number }>;
904905
dailySpendBreakdown: Array<{ date: string; email: string; name: string; spend_cents: number }>;
@@ -1194,7 +1195,8 @@ export function getFullDashboard(days: number = 7): FullDashboard {
11941195
SELECT pm.most_used_model as model, COUNT(*) as users,
11951196
ROUND(AVG(COALESCE(us.spend_cents, 0))/100, 0) as avg_spend,
11961197
ROUND(SUM(COALESCE(us.spend_cents, 0))/100, 0) as total_spend,
1197-
COALESCE(SUM(du.reqs), 0) as total_reqs
1198+
COALESCE(SUM(du.reqs), 0) as total_reqs,
1199+
GROUP_CONCAT(pm.email) as emails
11981200
FROM primary_model pm
11991201
LEFT JOIN user_spend us ON pm.email = us.email
12001202
LEFT JOIN (SELECT email, SUM(agent_requests) as reqs FROM daily_usage WHERE date >= date('now', ?) GROUP BY email) du ON pm.email = du.email
@@ -1208,6 +1210,7 @@ export function getFullDashboard(days: number = 7): FullDashboard {
12081210
avg_spend: number;
12091211
total_spend: number;
12101212
total_reqs: number;
1213+
emails: string;
12111214
}>;
12121215

12131216
const stats: DashboardStats = {
@@ -1223,7 +1226,16 @@ export function getFullDashboard(days: number = 7): FullDashboard {
12231226
rankedUsers: assignBadges(rankedUsers, days).map((u, i) => ({ ...u, rank: i + 1 })),
12241227
};
12251228

1226-
return { days, stats, modelCosts, teamDailySpend, dailySpendBreakdown };
1229+
return {
1230+
days,
1231+
stats,
1232+
modelCosts: modelCosts.map((mc) => ({
1233+
...mc,
1234+
emails: mc.emails ? mc.emails.split(",") : [],
1235+
})),
1236+
teamDailySpend,
1237+
dailySpendBreakdown,
1238+
};
12271239
}
12281240

12291241
function isMaxModel(model: string): boolean {
@@ -1714,9 +1726,10 @@ export function getModelCostBreakdown(): Array<{
17141726
avg_spend: number;
17151727
total_spend: number;
17161728
total_reqs: number;
1729+
emails: string[];
17171730
}> {
17181731
const db = getDb();
1719-
return db
1732+
const rows = db
17201733
.prepare(
17211734
`
17221735
WITH user_model AS (
@@ -1731,7 +1744,8 @@ export function getModelCostBreakdown(): Array<{
17311744
SELECT pm.most_used_model as model, COUNT(*) as users,
17321745
ROUND(AVG(s.spend_cents)/100, 0) as avg_spend,
17331746
ROUND(SUM(s.spend_cents)/100, 0) as total_spend,
1734-
COALESCE(SUM(du.reqs), 0) as total_reqs
1747+
COALESCE(SUM(du.reqs), 0) as total_reqs,
1748+
GROUP_CONCAT(pm.email) as emails
17351749
FROM primary_model pm
17361750
JOIN spending s ON pm.email = s.email
17371751
LEFT JOIN (SELECT email, SUM(agent_requests) as reqs FROM daily_usage GROUP BY email) du ON pm.email = du.email
@@ -1745,7 +1759,9 @@ export function getModelCostBreakdown(): Array<{
17451759
avg_spend: number;
17461760
total_spend: number;
17471761
total_reqs: number;
1762+
emails: string;
17481763
}>;
1764+
return rows.map((r) => ({ ...r, emails: r.emails ? r.emails.split(",") : [] }));
17491765
}
17501766

17511767
export function getTeamDailySpend(): Array<{ date: string; spend_cents: number }> {

0 commit comments

Comments
 (0)