Skip to content

Commit b231346

Browse files
committed
feat: lmarea
1 parent 6461f83 commit b231346

File tree

7 files changed

+348
-11
lines changed

7 files changed

+348
-11
lines changed

public/locales/en/rank.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,18 @@
5757
"total": "Total",
5858
"md_change": "📊"
5959
}
60+
},
61+
"lmrank": {
62+
"title": "LM Rank",
63+
"nav": "{{month}} {{year}} LM Ranking",
64+
"p_text": "<strong>The LLM Arena Ranking</strong> is a crowdsourced leaderboard for large language models. Users chat with two anonymous models and vote for the better response, with model ratings calculated using the Elo rating system. The leaderboard covers multiple capability dimensions including text, vision, and code, making it one of the most authoritative LLM evaluation benchmarks. Based on this ranking, we have done model name aggregation and cleaning work.",
65+
"thead": {
66+
"position": "Rank",
67+
"name": "Model",
68+
"organization": "Organization",
69+
"rating": "Score",
70+
"change": "Change",
71+
"md_change": "📊"
72+
}
6073
}
6174
}

public/locales/zh/rank.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,18 @@
5757
"total": "总贡献",
5858
"md_change": "本月"
5959
}
60+
},
61+
"lmrank": {
62+
"title": "大模型排名",
63+
"nav": "{{year}} 年 {{month}} 月大模型排行榜",
64+
"p_text": "<strong>「LLM Arena 排名」</strong>是基于众包用户投票的大语言模型排行榜。通过让用户与两个匿名模型对话并选择更好的回答,使用 Elo 评分系统计算模型的相对实力。该排行榜覆盖文本、视觉、代码等多个能力维度,是目前最权威的 LLM 评测榜单之一,基于此榜单我们做了模型名称聚合和清理工作。",
65+
"thead": {
66+
"position": "排名",
67+
"name": "模型",
68+
"organization": "机构",
69+
"rating": "分数",
70+
"change": "对比上月",
71+
"md_change": "趋势"
72+
}
6073
}
6174
}

src/components/rankTable/RankTable.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@ export const RankSearchBar = ({
115115
? [
116116
{ key: '/report/tiobe', value: 'Language' },
117117
{ key: '/report/contribution', value: 'Contribution' },
118-
{ key: '/report/netcraft', value: 'Server' },
118+
{ key: '/report/lm-rank', value: 'Model' },
119119
{ key: '/report/db-engines', value: 'Database' },
120120
]
121121
: [
122122
{ key: '/report/tiobe', value: '编程语言' },
123123
{ key: '/report/contribution', value: '用户贡献' },
124-
{ key: '/report/netcraft', value: '服务器' },
124+
{ key: '/report/lm-rank', value: '大模型' },
125125
{ key: '/report/db-engines', value: '数据库' },
126126
];
127127
}
@@ -135,26 +135,34 @@ export const RankSearchBar = ({
135135
}, [monthList, i18n_lang]);
136136

137137
return (
138-
<div className='mb-2 flex items-center justify-between rounded-lg border bg-gray-50 py-2 px-2 shadow dark:border-gray-700 dark:bg-gray-800 dark:shadow-gray-800'>
139-
<div className='justify-items-start'>
138+
<div className='mb-2 flex items-center rounded-lg border bg-gray-50 py-2 px-2 shadow dark:border-gray-700 dark:bg-gray-800 dark:shadow-gray-800'>
139+
<div className='flex flex-1 justify-start'>
140140
<Dropdown
141141
options={typeOptions}
142142
initValue={target}
143143
size='small'
144144
onChange={(opt) => onChange('target', opt.key)}
145145
/>
146146
</div>
147-
<div className=' justify-items-center'>
147+
<div className='flex justify-center'>
148148
<div className='flex items-center'>
149149
<div className='inline'>
150-
<img className='h-5 w-5' src={logo} alt='rank_logo' />
150+
<img
151+
className={`h-5 w-5 cursor-pointer hover:animate-spin ${
152+
title === 'HelloGitHub' || title === 'LMArena'
153+
? 'dark:invert'
154+
: ''
155+
}`}
156+
src={logo}
157+
alt='rank_logo'
158+
/>
151159
</div>
152160
<span className='ml-1 hidden dark:text-gray-300 md:block'>
153161
{title}
154162
</span>
155163
</div>
156164
</div>
157-
<div className='justify-items-end'>
165+
<div className='flex flex-1 justify-end'>
158166
<Dropdown
159167
initValue={month}
160168
size='small'

src/components/report/Report.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ export const ChangeColumnRender = (
1212
showPercent = false,
1313
i18n_lang: string
1414
) => {
15-
let text = '-';
16-
if (row.change !== null) {
17-
text = showPercent ? `${row.change}%` : `${row.change}`;
15+
if (row.change === null) {
16+
return <span>-</span>;
1817
}
1918

19+
const text = showPercent ? `${row.change}%` : `${row.change}`;
20+
2021
return (
2122
<div className='flex items-center'>
2223
{i18n_lang === 'en' ? (
@@ -59,6 +60,36 @@ export const TrendColumnRender = (
5960
return <span>{icon}</span>;
6061
};
6162

63+
// 分数+趋势合并列
64+
export const RatingWithTrendRender = (
65+
row: RankDataItem,
66+
_showPercent: boolean,
67+
i18n_lang: string
68+
) => {
69+
let icon = null;
70+
if (row.change !== null) {
71+
if (row.change > 0) {
72+
if (i18n_lang === 'en') {
73+
icon = <IoMdTrendingUp size={14} className='text-green-500' />;
74+
} else {
75+
icon = <IoMdTrendingUp size={14} className='text-red-500' />;
76+
}
77+
} else {
78+
if (i18n_lang === 'en') {
79+
icon = <IoMdTrendingDown size={14} className='text-red-500' />;
80+
} else {
81+
icon = <IoMdTrendingDown size={14} className='text-green-500' />;
82+
}
83+
}
84+
}
85+
return (
86+
<div className='flex items-center gap-1'>
87+
<span>{row.rating}</span>
88+
{icon}
89+
</div>
90+
);
91+
};
92+
6293
export const ContributionColumnRender = (
6394
row: RankDataItem,
6495
_showPercent: boolean,

src/pages/report/lm-rank.tsx

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { GetServerSideProps, NextPage } from 'next';
2+
import { useRouter } from 'next/router';
3+
import { Trans, useTranslation } from 'next-i18next';
4+
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
5+
import { useMemo } from 'react';
6+
7+
import Loading from '@/components/loading/Loading';
8+
import Navbar from '@/components/navbar/Navbar';
9+
import {
10+
getMonthName,
11+
RankSearchBar,
12+
RankTable,
13+
} from '@/components/rankTable/RankTable';
14+
import { RatingWithTrendRender } from '@/components/report/Report';
15+
import Seo from '@/components/Seo';
16+
17+
import { getLMArenaRank } from '@/services/rank';
18+
import { getClientIP } from '@/utils/util';
19+
20+
import { LMArenaRankPageProps } from '@/types/rank';
21+
22+
// 类别名称映射
23+
const getCategoryName = (category: string, lang: string) => {
24+
const categoryMap: Record<string, { en: string; zh: string }> = {
25+
text: { en: 'Text', zh: '文本' },
26+
webdev: { en: 'WebDev', zh: '网页开发' },
27+
vision: { en: 'Vision', zh: '视觉' },
28+
'text-to-image': { en: 'Text to Image', zh: '文生图' },
29+
'text-to-video': { en: 'Text to Video', zh: '文生视频' },
30+
};
31+
return categoryMap[category]?.[lang === 'en' ? 'en' : 'zh'] || category;
32+
};
33+
34+
const LMRankPage: NextPage<LMArenaRankPageProps> = ({
35+
year,
36+
month,
37+
monthList,
38+
category,
39+
categoryList,
40+
list,
41+
}) => {
42+
const { t, i18n } = useTranslation('rank');
43+
const router = useRouter();
44+
45+
const onSearch = (key: string, value: string) => {
46+
const currentQuery = { ...router.query };
47+
if (key === 'month') {
48+
currentQuery.month = value;
49+
}
50+
if (key === 'target') {
51+
router.push(`${value}`);
52+
return;
53+
}
54+
router.push({
55+
pathname: '/report/lm-rank',
56+
query: currentQuery,
57+
});
58+
};
59+
60+
const onCategoryChange = (opt: { key: string; value: string }) => {
61+
const currentQuery = { ...router.query };
62+
currentQuery.category = opt.key;
63+
router.push({
64+
pathname: '/report/lm-rank',
65+
query: currentQuery,
66+
});
67+
};
68+
69+
// 类别选项
70+
const categoryOptions = useMemo(() => {
71+
return categoryList?.map((cat) => ({
72+
key: cat,
73+
value: getCategoryName(cat, i18n.language),
74+
}));
75+
}, [categoryList, i18n.language]);
76+
77+
// 根据最长模型名称计算宽度
78+
const maxNameWidth = useMemo(() => {
79+
if (!list?.length) return 200;
80+
const maxLen = Math.max(...list.map((item) => item.name?.length || 0));
81+
// 每个字符约 8px,最小 120px,最大 360px
82+
return Math.min(Math.max(maxLen * 8, 120), 380);
83+
}, [list, category]);
84+
85+
// 表格列配置
86+
const columns: any[] = useMemo(
87+
() => [
88+
{ key: 'position', title: t('lmrank.thead.position'), width: 80 },
89+
{ key: 'name', title: t('lmrank.thead.name'), width: maxNameWidth },
90+
{
91+
key: 'rating',
92+
title: t('lmrank.thead.rating'),
93+
render: RatingWithTrendRender,
94+
width: 120,
95+
},
96+
{
97+
key: 'organization',
98+
title: t('lmrank.thead.organization'),
99+
width: 140,
100+
},
101+
],
102+
[i18n.language, maxNameWidth]
103+
);
104+
105+
// 移动端列配置(分数和趋势合并为一列)
106+
const md_columns: any[] = useMemo(
107+
() => [
108+
{ key: 'position', title: t('lmrank.thead.position'), width: 50 },
109+
{ key: 'name', title: t('lmrank.thead.name') },
110+
{
111+
key: 'rating',
112+
title: t('lmrank.thead.rating'),
113+
render: RatingWithTrendRender,
114+
width: 80,
115+
},
116+
],
117+
[i18n.language]
118+
);
119+
120+
return (
121+
<>
122+
<Seo title={t('lmrank.title')} />
123+
{list ? (
124+
<div>
125+
<Navbar
126+
middleText={t('lmrank.nav', {
127+
year: year,
128+
month: getMonthName(month, i18n.language, { forceEnglish: true }),
129+
})}
130+
/>
131+
<div className='my-2 bg-white px-2 pt-2 dark:bg-gray-800 md:rounded-lg'>
132+
<RankSearchBar
133+
title='LMArena'
134+
logo='https://img.hellogithub.com/logo/lmarena.png!small'
135+
i18n_lang={i18n.language}
136+
monthList={monthList}
137+
onChange={onSearch}
138+
/>
139+
<div className='mb-2 flex items-center justify-center'>
140+
<div className='flex flex-wrap items-center gap-1 md:gap-4'>
141+
{categoryOptions?.map((opt) => (
142+
<button
143+
key={opt.key}
144+
onClick={() => onCategoryChange(opt)}
145+
className={`rounded-md px-2 py-1 text-xs font-medium transition-colors md:px-3 md:py-1.5 md:text-sm ${
146+
category === opt.key
147+
? 'bg-blue-500 text-white'
148+
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
149+
}`}
150+
>
151+
{opt.value}
152+
</button>
153+
))}
154+
</div>
155+
</div>
156+
<div className='md:hidden'>
157+
<RankTable
158+
key={`mobile-${category}`}
159+
columns={md_columns}
160+
list={list}
161+
i18n_lang={i18n.language}
162+
/>
163+
</div>
164+
<div className='hidden md:block'>
165+
<RankTable
166+
key={`desktop-${category}`}
167+
columns={columns}
168+
list={list}
169+
i18n_lang={i18n.language}
170+
/>
171+
</div>
172+
<div className='mt-2 rounded-lg border bg-white p-2 text-sm dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300'>
173+
<div className='whitespace-pre-wrap leading-8'>
174+
<p>
175+
<Trans ns='rank' i18nKey='lmrank.p_text' />
176+
</p>
177+
</div>
178+
</div>
179+
<div className='h-2' />
180+
</div>
181+
</div>
182+
) : (
183+
<Loading />
184+
)}
185+
</>
186+
);
187+
};
188+
189+
export const getServerSideProps: GetServerSideProps = async ({
190+
query,
191+
req,
192+
locale,
193+
}) => {
194+
const ip = getClientIP(req);
195+
const data = await getLMArenaRank(
196+
ip,
197+
query['month'] as unknown as number,
198+
query['category'] as string
199+
);
200+
if (!data.success) {
201+
return {
202+
notFound: true,
203+
};
204+
} else {
205+
return {
206+
props: {
207+
year: data.year,
208+
month: data.month,
209+
list: data.data,
210+
monthList: data.month_list,
211+
category: data.category,
212+
categoryList: data.category_list,
213+
...(await serverSideTranslations(locale as string, ['common', 'rank'])),
214+
},
215+
};
216+
}
217+
};
218+
219+
export default LMRankPage;

0 commit comments

Comments
 (0)