Ko-TTS-Arena Contributors commited on
Commit
e1cb33d
·
1 Parent(s): aebf092

feat: Add category filters (한영혼합, 숫자혼합, 긴문장) to leaderboard

Browse files
Files changed (3) hide show
  1. app.py +11 -2
  2. models.py +95 -25
  3. templates/leaderboard.html +265 -39
app.py CHANGED
@@ -374,7 +374,12 @@ def arena():
374
 
375
  @app.route("/leaderboard")
376
  def leaderboard():
377
- tts_leaderboard = get_leaderboard_data(ModelType.TTS)
 
 
 
 
 
378
  voting_stats = get_voting_statistics() # Get voting statistics
379
 
380
  # Initialize personal leaderboard data
@@ -394,7 +399,11 @@ def leaderboard():
394
 
395
  return render_template(
396
  "leaderboard.html",
397
- tts_leaderboard=tts_leaderboard,
 
 
 
 
398
  tts_personal_leaderboard=tts_personal_leaderboard,
399
  tts_key_dates=tts_key_dates,
400
  formatted_tts_dates=formatted_tts_dates,
 
374
 
375
  @app.route("/leaderboard")
376
  def leaderboard():
377
+ # 카테고리별 리더보드 데이터
378
+ tts_leaderboard_all = get_leaderboard_data(ModelType.TTS, 'all')
379
+ tts_leaderboard_mixed_lang = get_leaderboard_data(ModelType.TTS, 'mixed_lang')
380
+ tts_leaderboard_with_numbers = get_leaderboard_data(ModelType.TTS, 'with_numbers')
381
+ tts_leaderboard_long_text = get_leaderboard_data(ModelType.TTS, 'long_text')
382
+
383
  voting_stats = get_voting_statistics() # Get voting statistics
384
 
385
  # Initialize personal leaderboard data
 
399
 
400
  return render_template(
401
  "leaderboard.html",
402
+ tts_leaderboard=tts_leaderboard_all,
403
+ tts_leaderboard_all=tts_leaderboard_all,
404
+ tts_leaderboard_mixed_lang=tts_leaderboard_mixed_lang,
405
+ tts_leaderboard_with_numbers=tts_leaderboard_with_numbers,
406
+ tts_leaderboard_long_text=tts_leaderboard_long_text,
407
  tts_personal_leaderboard=tts_personal_leaderboard,
408
  tts_key_dates=tts_key_dates,
409
  formatted_tts_dates=formatted_tts_dates,
models.py CHANGED
@@ -319,38 +319,57 @@ def record_vote(user_id, text, chosen_model_id, rejected_model_id, model_type,
319
  return vote, None
320
 
321
 
322
- def get_leaderboard_data(model_type):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  """
324
- Get leaderboard data for the specified model type.
325
  Only includes votes that count for the public leaderboard.
326
  Only shows active models.
327
 
328
  Args:
329
  model_type (str): The model type ('tts' or 'conversational')
 
330
 
331
  Returns:
332
  list: List of dictionaries containing model data for the leaderboard
333
  """
334
- query = Model.query.filter_by(model_type=model_type, is_active=True)
335
-
336
- # Get active models with >0 votes ordered by ELO score
337
- # Note: Model.match_count now only includes votes that count for public leaderboard
338
- models = query.filter(Model.match_count > 0).order_by(Model.current_elo.desc()).all()
339
-
340
- result = []
341
- for rank, model in enumerate(models, 1):
342
- # Determine tier based on rank
343
- if rank <= 2:
344
- tier = "tier-s"
345
- elif rank <= 4:
346
- tier = "tier-a"
347
- elif rank <= 7:
348
- tier = "tier-b"
349
- else:
350
- tier = ""
351
-
352
- result.append(
353
- {
354
  "rank": rank,
355
  "id": model.id,
356
  "name": model.name,
@@ -360,9 +379,60 @@ def get_leaderboard_data(model_type):
360
  "elo": int(model.current_elo),
361
  "tier": tier,
362
  "is_open": model.is_open,
363
- }
364
- )
365
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  return result
367
 
368
 
 
319
  return vote, None
320
 
321
 
322
+ import re
323
+
324
+ def categorize_text(text):
325
+ """
326
+ 텍스트를 카테고리로 분류
327
+ Returns: list of categories the text belongs to
328
+ """
329
+ categories = ['all'] # 모든 텍스트는 'all'에 포함
330
+
331
+ # 한영혼합: 알파벳 5글자 이상
332
+ alphabet_count = len(re.findall(r'[a-zA-Z]', text))
333
+ if alphabet_count >= 5:
334
+ categories.append('mixed_lang')
335
+
336
+ # 숫자혼합: 숫자 2개 이상
337
+ digit_count = len(re.findall(r'\d', text))
338
+ if digit_count >= 2:
339
+ categories.append('with_numbers')
340
+
341
+ # 긴문장: 50글자 이상
342
+ if len(text) >= 50:
343
+ categories.append('long_text')
344
+
345
+ return categories
346
+
347
+
348
+ def get_leaderboard_data(model_type, category='all'):
349
  """
350
+ Get leaderboard data for the specified model type and category.
351
  Only includes votes that count for the public leaderboard.
352
  Only shows active models.
353
 
354
  Args:
355
  model_type (str): The model type ('tts' or 'conversational')
356
+ category (str): Category filter ('all', 'mixed_lang', 'with_numbers', 'long_text')
357
 
358
  Returns:
359
  list: List of dictionaries containing model data for the leaderboard
360
  """
361
+ # 활성 모델 가져오기
362
+ active_models = Model.query.filter_by(model_type=model_type, is_active=True).all()
363
+
364
+ if category == 'all':
365
+ # 기존 로직: Model 테이블의 집계 데이터 사용
366
+ models = [m for m in active_models if m.match_count > 0]
367
+ models.sort(key=lambda x: x.current_elo, reverse=True)
368
+
369
+ result = []
370
+ for rank, model in enumerate(models, 1):
371
+ tier = "tier-s" if rank <= 2 else "tier-a" if rank <= 4 else "tier-b" if rank <= 7 else ""
372
+ result.append({
 
 
 
 
 
 
 
 
373
  "rank": rank,
374
  "id": model.id,
375
  "name": model.name,
 
379
  "elo": int(model.current_elo),
380
  "tier": tier,
381
  "is_open": model.is_open,
382
+ })
383
+ return result
384
+
385
+ # 카테고리별 필터링: Vote 테이블에서 직접 계산
386
+ active_model_ids = {m.id for m in active_models}
387
+
388
+ # 해당 카테고리에 맞는 투표 가져오기
389
+ all_votes = Vote.query.filter(
390
+ Vote.model_type == model_type,
391
+ Vote.counts_for_public_leaderboard == True
392
+ ).all()
393
+
394
+ # 카테고리에 맞는 투표만 필터링
395
+ filtered_votes = []
396
+ for vote in all_votes:
397
+ text_categories = categorize_text(vote.text)
398
+ if category in text_categories:
399
+ filtered_votes.append(vote)
400
+
401
+ # 모델별 승/패 계산
402
+ model_stats = {m.id: {"wins": 0, "matches": 0, "model": m} for m in active_models}
403
+
404
+ for vote in filtered_votes:
405
+ if vote.model_chosen in active_model_ids:
406
+ model_stats[vote.model_chosen]["wins"] += 1
407
+ model_stats[vote.model_chosen]["matches"] += 1
408
+ if vote.model_rejected in active_model_ids:
409
+ model_stats[vote.model_rejected]["matches"] += 1
410
+
411
+ # 승률 계산 및 정렬
412
+ result = []
413
+ for model_id, stats in model_stats.items():
414
+ if stats["matches"] > 0:
415
+ win_rate = (stats["wins"] / stats["matches"]) * 100
416
+ result.append({
417
+ "id": model_id,
418
+ "name": stats["model"].name,
419
+ "model_url": stats["model"].model_url,
420
+ "win_rate": f"{win_rate:.0f}%",
421
+ "total_votes": stats["matches"],
422
+ "elo": int(1500 + (win_rate - 50) * 10), # 간단한 ELO 추정
423
+ "is_open": stats["model"].is_open,
424
+ "_win_rate_num": win_rate,
425
+ })
426
+
427
+ # 승률로 정렬
428
+ result.sort(key=lambda x: x["_win_rate_num"], reverse=True)
429
+
430
+ # 랭크와 티어 추가
431
+ for rank, item in enumerate(result, 1):
432
+ item["rank"] = rank
433
+ item["tier"] = "tier-s" if rank <= 2 else "tier-a" if rank <= 4 else "tier-b" if rank <= 7 else ""
434
+ del item["_win_rate_num"]
435
+
436
  return result
437
 
438
 
templates/leaderboard.html CHANGED
@@ -919,6 +919,75 @@
919
  visibility: visible;
920
  opacity: 1;
921
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
922
  </style>
923
  {% endblock %}
924
 
@@ -994,52 +1063,177 @@
994
  </div>
995
 
996
  <div id="tts-public-leaderboard" class="leaderboard-view active">
997
- {% if tts_leaderboard and tts_leaderboard|length > 0 %}
998
- <div class="leaderboard-container">
999
- <div class="leaderboard-header">
1000
- <div>Rank</div>
1001
- <div>Model</div>
1002
- <div style="text-align: right">Win Rate</div>
1003
- <div style="text-align: right" class="total-votes-header">Total Votes</div>
1004
- <div style="text-align: right">ELO</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1005
  </div>
1006
-
1007
- {% for model in tts_leaderboard %}
1008
- <div class="leaderboard-row {{ model.tier }}">
1009
- <div class="rank">#{{ model.rank }}</div>
1010
- <div class="model-name">
1011
- <a href="{{ model.model_url }}" target="_blank" class="model-name-link">{{ model.name }}</a>
1012
- <div class="license-icon">
1013
- {% if not model.is_open %}
1014
- <img src="{{ url_for('static', filename='closed.svg') }}" alt="Proprietary">
1015
- <span class="tooltip">Proprietary model</span>
1016
- {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1017
  </div>
1018
- {% if model.id == "lanternfish-1" %}
1019
- <div class="disputed-badge">
1020
- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1021
- <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
1022
- <line x1="12" y1="9" x2="12" y2="13"/>
1023
- <line x1="12" y1="17" x2="12" y2="17"/>
1024
- </svg>
1025
- <span>Disputed</span>
1026
- <span class="tooltip">Potential vote manipulation</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1027
  </div>
1028
- {% endif %}
 
 
1029
  </div>
1030
- <div class="win-rate">{{ model.win_rate }}</div>
1031
- <div class="total-votes">{{ model.total_votes }}</div>
1032
- <div class="elo-score">{{ model.elo }}</div>
1033
  </div>
1034
- {% endfor %}
 
 
 
 
 
1035
  </div>
1036
- {% else %}
1037
- <div class="no-data">
1038
- <h3>No data available yet</h3>
1039
- <p>Be the first to vote and help build the leaderboard! Compare models in the arena to see how they stack up.</p>
1040
- <a href="{{ url_for('arena') }}" class="btn">Go to Arena</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1041
  </div>
1042
- {% endif %}
1043
  </div>
1044
 
1045
  <div id="tts-personal-leaderboard" class="leaderboard-view" style="display: none;">
@@ -1440,6 +1634,38 @@
1440
  setupHistoricalView('tts');
1441
  setupHistoricalView('conversational');
1442
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1443
 
1444
  // Final positioning after all DOM operations are complete
1445
  setTimeout(positionSliders, 100);
 
919
  visibility: visible;
920
  opacity: 1;
921
  }
922
+
923
+ /* Category filter styles */
924
+ .category-filter {
925
+ display: flex;
926
+ gap: 8px;
927
+ margin-bottom: 20px;
928
+ flex-wrap: wrap;
929
+ justify-content: center;
930
+ }
931
+
932
+ .category-btn {
933
+ padding: 8px 16px;
934
+ border: 2px solid var(--border-color);
935
+ border-radius: 20px;
936
+ background: white;
937
+ color: var(--text-color);
938
+ font-size: 13px;
939
+ font-weight: 500;
940
+ cursor: pointer;
941
+ transition: all 0.2s ease;
942
+ display: flex;
943
+ align-items: center;
944
+ gap: 6px;
945
+ }
946
+
947
+ .category-btn:hover {
948
+ border-color: var(--primary-color);
949
+ color: var(--primary-color);
950
+ }
951
+
952
+ .category-btn.active {
953
+ background: var(--primary-color);
954
+ border-color: var(--primary-color);
955
+ color: white;
956
+ }
957
+
958
+ .category-btn .category-count {
959
+ background: rgba(0, 0, 0, 0.1);
960
+ padding: 2px 8px;
961
+ border-radius: 10px;
962
+ font-size: 11px;
963
+ }
964
+
965
+ .category-btn.active .category-count {
966
+ background: rgba(255, 255, 255, 0.2);
967
+ }
968
+
969
+ .category-description {
970
+ text-align: center;
971
+ font-size: 13px;
972
+ color: #888;
973
+ margin-bottom: 16px;
974
+ }
975
+
976
+ @media (prefers-color-scheme: dark) {
977
+ .category-btn {
978
+ background: var(--light-gray);
979
+ border-color: var(--border-color);
980
+ }
981
+
982
+ .category-btn:hover {
983
+ border-color: var(--primary-color);
984
+ }
985
+
986
+ .category-btn.active {
987
+ background: var(--primary-color);
988
+ border-color: var(--primary-color);
989
+ }
990
+ }
991
  </style>
992
  {% endblock %}
993
 
 
1063
  </div>
1064
 
1065
  <div id="tts-public-leaderboard" class="leaderboard-view active">
1066
+ <!-- Category Filter -->
1067
+ <div class="category-filter">
1068
+ <button class="category-btn active" data-category="all">
1069
+ 📊 전체
1070
+ <span class="category-count">{{ tts_leaderboard_all|length }}</span>
1071
+ </button>
1072
+ <button class="category-btn" data-category="mixed_lang">
1073
+ 🔤 한영혼합
1074
+ <span class="category-count">{{ tts_leaderboard_mixed_lang|length }}</span>
1075
+ </button>
1076
+ <button class="category-btn" data-category="with_numbers">
1077
+ 🔢 숫자혼합
1078
+ <span class="category-count">{{ tts_leaderboard_with_numbers|length }}</span>
1079
+ </button>
1080
+ <button class="category-btn" data-category="long_text">
1081
+ 📝 긴문장
1082
+ <span class="category-count">{{ tts_leaderboard_long_text|length }}</span>
1083
+ </button>
1084
+ </div>
1085
+ <div class="category-description" id="category-description">
1086
+ 모든 투표 기준 순위
1087
+ </div>
1088
+
1089
+ <!-- All Category Leaderboard -->
1090
+ <div id="leaderboard-all" class="category-leaderboard">
1091
+ {% if tts_leaderboard_all and tts_leaderboard_all|length > 0 %}
1092
+ <div class="leaderboard-container">
1093
+ <div class="leaderboard-header">
1094
+ <div>Rank</div>
1095
+ <div>Model</div>
1096
+ <div style="text-align: right">Win Rate</div>
1097
+ <div style="text-align: right" class="total-votes-header">Total Votes</div>
1098
+ <div style="text-align: right">ELO</div>
1099
+ </div>
1100
+ {% for model in tts_leaderboard_all %}
1101
+ <div class="leaderboard-row {{ model.tier }}">
1102
+ <div class="rank">#{{ model.rank }}</div>
1103
+ <div class="model-name">
1104
+ <a href="{{ model.model_url }}" target="_blank" class="model-name-link">{{ model.name }}</a>
1105
+ <div class="license-icon">
1106
+ {% if not model.is_open %}
1107
+ <img src="{{ url_for('static', filename='closed.svg') }}" alt="Proprietary">
1108
+ <span class="tooltip">Proprietary model</span>
1109
+ {% endif %}
1110
+ </div>
1111
+ </div>
1112
+ <div class="win-rate">{{ model.win_rate }}</div>
1113
+ <div class="total-votes">{{ model.total_votes }}</div>
1114
+ <div class="elo-score">{{ model.elo }}</div>
1115
+ </div>
1116
+ {% endfor %}
1117
  </div>
1118
+ {% else %}
1119
+ <div class="no-data">
1120
+ <h3>No data available yet</h3>
1121
+ <p>Be the first to vote and help build the leaderboard!</p>
1122
+ <a href="{{ url_for('arena') }}" class="btn">Go to Arena</a>
1123
+ </div>
1124
+ {% endif %}
1125
+ </div>
1126
+
1127
+ <!-- Mixed Language Leaderboard -->
1128
+ <div id="leaderboard-mixed_lang" class="category-leaderboard" style="display: none;">
1129
+ {% if tts_leaderboard_mixed_lang and tts_leaderboard_mixed_lang|length > 0 %}
1130
+ <div class="leaderboard-container">
1131
+ <div class="leaderboard-header">
1132
+ <div>Rank</div>
1133
+ <div>Model</div>
1134
+ <div style="text-align: right">Win Rate</div>
1135
+ <div style="text-align: right" class="total-votes-header">Total Votes</div>
1136
+ <div style="text-align: right">ELO</div>
1137
+ </div>
1138
+ {% for model in tts_leaderboard_mixed_lang %}
1139
+ <div class="leaderboard-row {{ model.tier }}">
1140
+ <div class="rank">#{{ model.rank }}</div>
1141
+ <div class="model-name">
1142
+ <a href="{{ model.model_url }}" target="_blank" class="model-name-link">{{ model.name }}</a>
1143
+ <div class="license-icon">
1144
+ {% if not model.is_open %}
1145
+ <img src="{{ url_for('static', filename='closed.svg') }}" alt="Proprietary">
1146
+ <span class="tooltip">Proprietary model</span>
1147
+ {% endif %}
1148
+ </div>
1149
  </div>
1150
+ <div class="win-rate">{{ model.win_rate }}</div>
1151
+ <div class="total-votes">{{ model.total_votes }}</div>
1152
+ <div class="elo-score">{{ model.elo }}</div>
1153
+ </div>
1154
+ {% endfor %}
1155
+ </div>
1156
+ {% else %}
1157
+ <div class="no-data">
1158
+ <h3>한영혼합 데이터가 없습니다</h3>
1159
+ <p>알파벳이 5글자 이상 포함된 문장에 대한 투표가 아직 없습니다.</p>
1160
+ </div>
1161
+ {% endif %}
1162
+ </div>
1163
+
1164
+ <!-- Numbers Leaderboard -->
1165
+ <div id="leaderboard-with_numbers" class="category-leaderboard" style="display: none;">
1166
+ {% if tts_leaderboard_with_numbers and tts_leaderboard_with_numbers|length > 0 %}
1167
+ <div class="leaderboard-container">
1168
+ <div class="leaderboard-header">
1169
+ <div>Rank</div>
1170
+ <div>Model</div>
1171
+ <div style="text-align: right">Win Rate</div>
1172
+ <div style="text-align: right" class="total-votes-header">Total Votes</div>
1173
+ <div style="text-align: right">ELO</div>
1174
+ </div>
1175
+ {% for model in tts_leaderboard_with_numbers %}
1176
+ <div class="leaderboard-row {{ model.tier }}">
1177
+ <div class="rank">#{{ model.rank }}</div>
1178
+ <div class="model-name">
1179
+ <a href="{{ model.model_url }}" target="_blank" class="model-name-link">{{ model.name }}</a>
1180
+ <div class="license-icon">
1181
+ {% if not model.is_open %}
1182
+ <img src="{{ url_for('static', filename='closed.svg') }}" alt="Proprietary">
1183
+ <span class="tooltip">Proprietary model</span>
1184
+ {% endif %}
1185
+ </div>
1186
  </div>
1187
+ <div class="win-rate">{{ model.win_rate }}</div>
1188
+ <div class="total-votes">{{ model.total_votes }}</div>
1189
+ <div class="elo-score">{{ model.elo }}</div>
1190
  </div>
1191
+ {% endfor %}
 
 
1192
  </div>
1193
+ {% else %}
1194
+ <div class="no-data">
1195
+ <h3>숫자혼합 데이터가 없습니다</h3>
1196
+ <p>숫자가 2개 이상 포함된 문장에 대한 투표가 아직 없습니다.</p>
1197
+ </div>
1198
+ {% endif %}
1199
  </div>
1200
+
1201
+ <!-- Long Text Leaderboard -->
1202
+ <div id="leaderboard-long_text" class="category-leaderboard" style="display: none;">
1203
+ {% if tts_leaderboard_long_text and tts_leaderboard_long_text|length > 0 %}
1204
+ <div class="leaderboard-container">
1205
+ <div class="leaderboard-header">
1206
+ <div>Rank</div>
1207
+ <div>Model</div>
1208
+ <div style="text-align: right">Win Rate</div>
1209
+ <div style="text-align: right" class="total-votes-header">Total Votes</div>
1210
+ <div style="text-align: right">ELO</div>
1211
+ </div>
1212
+ {% for model in tts_leaderboard_long_text %}
1213
+ <div class="leaderboard-row {{ model.tier }}">
1214
+ <div class="rank">#{{ model.rank }}</div>
1215
+ <div class="model-name">
1216
+ <a href="{{ model.model_url }}" target="_blank" class="model-name-link">{{ model.name }}</a>
1217
+ <div class="license-icon">
1218
+ {% if not model.is_open %}
1219
+ <img src="{{ url_for('static', filename='closed.svg') }}" alt="Proprietary">
1220
+ <span class="tooltip">Proprietary model</span>
1221
+ {% endif %}
1222
+ </div>
1223
+ </div>
1224
+ <div class="win-rate">{{ model.win_rate }}</div>
1225
+ <div class="total-votes">{{ model.total_votes }}</div>
1226
+ <div class="elo-score">{{ model.elo }}</div>
1227
+ </div>
1228
+ {% endfor %}
1229
+ </div>
1230
+ {% else %}
1231
+ <div class="no-data">
1232
+ <h3>긴문장 데이터가 없습니다</h3>
1233
+ <p>50글자 이상인 문장에 대한 투표가 아직 없습니다.</p>
1234
+ </div>
1235
+ {% endif %}
1236
  </div>
 
1237
  </div>
1238
 
1239
  <div id="tts-personal-leaderboard" class="leaderboard-view" style="display: none;">
 
1634
  setupHistoricalView('tts');
1635
  setupHistoricalView('conversational');
1636
  */
1637
+
1638
+ // Category filter functionality
1639
+ const categoryBtns = document.querySelectorAll('.category-btn');
1640
+ const categoryLeaderboards = document.querySelectorAll('.category-leaderboard');
1641
+ const categoryDescription = document.getElementById('category-description');
1642
+
1643
+ const categoryDescriptions = {
1644
+ 'all': '모든 투표 기준 순위',
1645
+ 'mixed_lang': '알파벳 5글자 이상 포함된 문장 기준',
1646
+ 'with_numbers': '숫자 2개 이상 포함된 문장 기준',
1647
+ 'long_text': '50글자 이상 긴 문장 기준'
1648
+ };
1649
+
1650
+ categoryBtns.forEach(btn => {
1651
+ btn.addEventListener('click', function() {
1652
+ const category = this.dataset.category;
1653
+
1654
+ // Update active button
1655
+ categoryBtns.forEach(b => b.classList.remove('active'));
1656
+ this.classList.add('active');
1657
+
1658
+ // Update description
1659
+ if (categoryDescription) {
1660
+ categoryDescription.textContent = categoryDescriptions[category] || '';
1661
+ }
1662
+
1663
+ // Show/hide leaderboards
1664
+ categoryLeaderboards.forEach(lb => {
1665
+ lb.style.display = lb.id === `leaderboard-${category}` ? 'block' : 'none';
1666
+ });
1667
+ });
1668
+ });
1669
 
1670
  // Final positioning after all DOM operations are complete
1671
  setTimeout(positionSliders, 100);