Skip to content

Commit d3fdb92

Browse files
authored
Merge pull request #676 from Merit-Systems/rfs/leaderboard
Switch leaderbaord ranking strat
2 parents 51ce1a5 + 6c48270 commit d3fdb92

6 files changed

Lines changed: 188 additions & 33 deletions

File tree

packages/app/control/src/app/(app)/(home)/top-apps/_components/apps.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import {
99
LoadingAppCard,
1010
} from '@/app/(app)/_components/apps/card/horizontal';
1111

12-
import { api } from '@/trpc/client';
1312
import { Button } from '@/components/ui/button';
14-
import { Info, Loader2 } from 'lucide-react';
1513
import { Card } from '@/components/ui/card';
14+
import { api } from '@/trpc/client';
15+
import { Info, Loader2 } from 'lucide-react';
1616

1717
export const TopApps = () => {
1818
return (
@@ -30,7 +30,7 @@ const Apps = () => {
3030
const [apps, { hasNextPage, fetchNextPage, isFetchingNextPage }] =
3131
api.apps.list.public.useSuspenseInfiniteQuery(
3232
{
33-
page_size: 10,
33+
page_size: 20,
3434
},
3535
{
3636
getNextPageParam: lastPage =>
@@ -53,9 +53,20 @@ const Apps = () => {
5353

5454
return (
5555
<AppsContainer>
56-
{items.map(app => (
57-
<AppCard key={app.id} {...app} />
58-
))}
56+
<AppsHeader />
57+
{items.map(app => {
58+
if (!app) return null;
59+
return (
60+
<AppCard
61+
key={app.id}
62+
id={app.id}
63+
name={app.name}
64+
description={app.description}
65+
profilePictureUrl={app.profilePictureUrl}
66+
homepageUrl={app.homepageUrl}
67+
/>
68+
);
69+
})}
5970
{hasNextPage && (
6071
<Button
6172
onClick={() => fetchNextPage()}
@@ -76,13 +87,29 @@ const Apps = () => {
7687
export const LoadingApps = () => {
7788
return (
7889
<AppsContainer>
90+
<AppsHeader />
7991
{Array.from({ length: 3 }).map((_, i) => (
8092
<LoadingAppCard key={i} />
8193
))}
8294
</AppsContainer>
8395
);
8496
};
8597

98+
const AppsHeader = () => {
99+
return (
100+
<div className="grid grid-cols-4 sm:grid-cols-6 lg:grid-cols-8 gap-2 sm:gap-4 px-4 pb-2 text-sm font-semibold text-muted-foreground">
101+
<div className="col-span-2 sm:col-span-3">App</div>
102+
<div className="hidden lg:flex justify-center col-span-1">Users</div>
103+
<div className="hidden sm:flex justify-center col-span-1">
104+
Transactions
105+
</div>
106+
<div className="hidden lg:flex justify-center col-span-1">Tokens</div>
107+
<div className="flex justify-center col-span-1">Usage</div>
108+
<div className="flex justify-end col-span-1">Earnings</div>
109+
</div>
110+
);
111+
};
112+
86113
const AppsContainer = ({ children }: { children: React.ReactNode }) => {
87114
return <div className="flex flex-col gap-4">{children}</div>;
88115
};

packages/app/control/src/app/(app)/(home)/top-apps/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ export default async function TopAppsPage(props: PageProps<'/top-apps'>) {
1515
await userOrRedirect('/top-apps', props);
1616

1717
void api.apps.list.public.prefetchInfinite({
18-
page_size: 10,
18+
page_size: 20,
1919
});
2020

2121
return (
2222
<HydrateClient>
23-
<Heading title="Top Apps" description="Echo apps with the most users" />
23+
<Heading
24+
title="Top Apps"
25+
description="Echo apps with the highest LLM usage"
26+
/>
2427
<Body>
2528
<TopApps />
2629
</Body>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client';
2+
3+
import React from 'react';
4+
5+
import { DollarSign } from 'lucide-react';
6+
7+
import { LoadingMetric, Metric } from './metric';
8+
9+
import { api } from '@/trpc/client';
10+
11+
interface Props {
12+
appId: string;
13+
}
14+
15+
export const TotalCost: React.FC<Props> = ({ appId }) => {
16+
const { data: stats, isLoading } = api.apps.app.stats.overall.useQuery({
17+
appId,
18+
});
19+
20+
return (
21+
<Metric
22+
isLoading={isLoading}
23+
value={(stats?.totalCost ?? 0).toFixed(2)}
24+
Icon={DollarSign}
25+
className="text-muted-foreground"
26+
/>
27+
);
28+
};
29+
30+
export const LoadingTotalCost = () => {
31+
return <LoadingMetric Icon={DollarSign} className="text-muted-foreground" />;
32+
};

packages/app/control/src/app/(app)/_components/apps/card/horizontal/index.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import { Skeleton } from '@/components/ui/skeleton';
77

88
import { UserAvatar } from '@/components/utils/user-avatar';
99

10-
import { Users, LoadingUsers } from './users';
10+
import { LoadingTotalCost, TotalCost } from './cost';
1111
import { Earnings, LoadingEarningsAmount } from './earnings';
12+
import { LoadingTotalTokens, TotalTokens } from './tokens';
1213
import { LoadingTransactions, Transactions } from './transactions';
14+
import { LoadingUsers, Users } from './users';
1315

1416
import { cn } from '@/lib/utils';
1517

@@ -30,8 +32,8 @@ export const AppCard = ({
3032
}: Props) => {
3133
return (
3234
<Link href={`/app/${id}`}>
33-
<Card className="hover:border-primary/50 transition-colors p-4 grid grid-cols-6 gap-4">
34-
<div className="flex flex-col gap-2 col-span-3">
35+
<Card className="hover:border-primary/50 transition-colors p-4 grid grid-cols-4 sm:grid-cols-6 lg:grid-cols-8 gap-2 sm:gap-4">
36+
<div className="flex flex-col gap-2 col-span-2 sm:col-span-3">
3537
<div className="flex flex-row items-center gap-2 flex-1 overflow-hidden">
3638
<UserAvatar
3739
className="size-10 shrink-0"
@@ -44,7 +46,7 @@ export const AppCard = ({
4446
</h2>
4547
<p
4648
className={cn(
47-
'text-xs text-muted-foreground/60',
49+
'text-xs text-muted-foreground/60 hidden sm:block',
4850
homepageUrl ? 'text-foreground/80' : 'text-foreground/40'
4951
)}
5052
>
@@ -54,19 +56,25 @@ export const AppCard = ({
5456
</div>
5557
<p
5658
className={cn(
57-
'text-sm text-muted-foreground/60',
59+
'text-sm text-muted-foreground/60 hidden sm:block',
5860
!description && 'text-muted-foreground/40'
5961
)}
6062
>
6163
{description ?? 'No description'}
6264
</p>
6365
</div>
64-
<div className="flex items-center gap-2 justify-center col-span-1">
66+
<div className="hidden lg:flex items-center gap-2 justify-center col-span-1">
6567
<Users appId={id} />
6668
</div>
67-
<div className="shrink-0 flex items-center justify-center col-span-1">
69+
<div className="hidden sm:flex shrink-0 items-center justify-center col-span-1">
6870
<Transactions appId={id} />
6971
</div>
72+
<div className="hidden lg:flex shrink-0 items-center justify-center col-span-1">
73+
<TotalTokens appId={id} />
74+
</div>
75+
<div className="shrink-0 flex items-center justify-center col-span-1">
76+
<TotalCost appId={id} />
77+
</div>
7078
<div className="shrink-0 flex items-center justify-end col-span-1">
7179
<Earnings appId={id} />
7280
</div>
@@ -77,8 +85,8 @@ export const AppCard = ({
7785

7886
export const LoadingAppCard = () => {
7987
return (
80-
<Card className="p-4 grid grid-cols-6 gap-4">
81-
<div className="flex flex-col gap-2 col-span-3">
88+
<Card className="p-4 grid grid-cols-4 sm:grid-cols-6 lg:grid-cols-8 gap-2 sm:gap-4">
89+
<div className="flex flex-col gap-2 col-span-2 sm:col-span-3">
8290
<div className="flex items-center gap-2">
8391
<UserAvatar
8492
className="size-10 shrink-0"
@@ -87,17 +95,23 @@ export const LoadingAppCard = () => {
8795
/>
8896
<div className="flex flex-col">
8997
<Skeleton className="w-24 h-5 my-[2.5px]" />
90-
<Skeleton className="w-24 h-3 my-0.5" />
98+
<Skeleton className="w-24 h-3 my-0.5 hidden sm:block" />
9199
</div>
92100
</div>
93-
<Skeleton className="w-3/4 h-4 my-[2px]" />
101+
<Skeleton className="w-3/4 h-4 my-[2px] hidden sm:block" />
94102
</div>
95-
<div className="flex items-center gap-2 justify-center col-span-1">
103+
<div className="hidden lg:flex items-center gap-2 justify-center col-span-1">
96104
<LoadingUsers />
97105
</div>
98-
<div className="shrink-0 flex items-center justify-center col-span-1">
106+
<div className="hidden sm:flex shrink-0 items-center justify-center col-span-1">
99107
<LoadingTransactions />
100108
</div>
109+
<div className="hidden lg:flex shrink-0 items-center justify-center col-span-1">
110+
<LoadingTotalTokens />
111+
</div>
112+
<div className="shrink-0 flex items-center justify-center col-span-1">
113+
<LoadingTotalCost />
114+
</div>
101115
<div className="shrink-0 flex items-center justify-end col-span-1">
102116
<LoadingEarningsAmount />
103117
</div>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use client';
2+
3+
import React from 'react';
4+
5+
import { Braces } from 'lucide-react';
6+
7+
import { LoadingMetric, Metric } from './metric';
8+
9+
import { api } from '@/trpc/client';
10+
11+
interface Props {
12+
appId: string;
13+
}
14+
15+
export const TotalTokens: React.FC<Props> = ({ appId }) => {
16+
const { data: stats, isLoading } = api.apps.app.stats.overall.useQuery({
17+
appId,
18+
});
19+
20+
const totalTokens = stats?.totalTokens ?? 0;
21+
const formattedTokens =
22+
totalTokens >= 1_000_000
23+
? `${(totalTokens / 1_000_000).toFixed(1)}M`
24+
: totalTokens >= 1_000
25+
? `${(totalTokens / 1_000).toFixed(1)}K`
26+
: totalTokens.toString();
27+
28+
return <Metric isLoading={isLoading} value={formattedTokens} Icon={Braces} />;
29+
};
30+
31+
export const LoadingTotalTokens = () => {
32+
return <LoadingMetric Icon={Braces} />;
33+
};

packages/app/control/src/services/db/apps/list.ts

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,69 @@ export const listPublicApps = async (
3030
where.name = { contains: search, mode: 'insensitive' };
3131
}
3232

33+
// Get app IDs sorted by total cost using raw SQL
34+
let sortedAppIds: Array<{ id: string }>;
35+
36+
if (search) {
37+
sortedAppIds = await db.$queryRaw<Array<{ id: string }>>`
38+
SELECT e.id
39+
FROM echo_apps e
40+
LEFT JOIN (
41+
SELECT
42+
"echoAppId",
43+
COALESCE(SUM("totalCost"), 0) as total_cost
44+
FROM transactions
45+
WHERE "isArchived" = false
46+
GROUP BY "echoAppId"
47+
) t ON e.id = t."echoAppId"
48+
WHERE e."isPublic" = true
49+
AND e."isArchived" = false
50+
AND e.name ILIKE ${`%${search}%`}
51+
ORDER BY COALESCE(t.total_cost, 0) DESC
52+
LIMIT ${page_size}
53+
OFFSET ${page * page_size}
54+
`;
55+
} else {
56+
sortedAppIds = await db.$queryRaw<Array<{ id: string }>>`
57+
SELECT e.id
58+
FROM echo_apps e
59+
LEFT JOIN (
60+
SELECT
61+
"echoAppId",
62+
COALESCE(SUM("totalCost"), 0) as total_cost
63+
FROM transactions
64+
WHERE "isArchived" = false
65+
GROUP BY "echoAppId"
66+
) t ON e.id = t."echoAppId"
67+
WHERE e."isPublic" = true
68+
AND e."isArchived" = false
69+
ORDER BY COALESCE(t.total_cost, 0) DESC
70+
LIMIT ${page_size}
71+
OFFSET ${page * page_size}
72+
`;
73+
}
74+
75+
const appIds = sortedAppIds.map(row => row.id);
76+
77+
// Fetch full app data maintaining the sort order
3378
const [apps, totalCount] = await Promise.all([
34-
db.echoApp.findMany({
35-
where,
36-
skip: page * page_size,
37-
take: page_size,
38-
select: appSelect,
39-
orderBy: {
40-
appMemberships: {
41-
_count: 'desc',
42-
},
43-
},
44-
}),
79+
appIds.length > 0
80+
? db.echoApp.findMany({
81+
where: { id: { in: appIds } },
82+
select: appSelect,
83+
})
84+
: [],
4585
countApps(where),
4686
]);
4787

88+
// Sort apps to match the original order from the query
89+
const appsMap = new Map(apps.map(app => [app.id, app]));
90+
const sortedApps = appIds
91+
.map(id => appsMap.get(id))
92+
.filter((app): app is NonNullable<typeof app> => app !== undefined);
93+
4894
return toPaginatedReponse({
49-
items: apps,
95+
items: sortedApps,
5096
page,
5197
page_size,
5298
total_count: totalCount,

0 commit comments

Comments
 (0)