Spaces:
Running
Running
openhands
openhands
commited on
Commit
·
c14a283
1
Parent(s):
71ba49b
Add Winners by Category section to main page
Browse files- Implemented new 'Winners by Category' section displaying top 5 systems for each benchmark category
- Added functions get_winners_by_category() and create_winners_by_category_html() to ui_components.py
- Created responsive grid layout with category cards showing:
- Category icon and name header
- Ranked table with medal emojis (🥇🥈🥉) for top 3
- Company logos for model providers
- SDK version and model name
- Category score
- Added comprehensive CSS styles in content.py:
- Light and dark mode support
- Responsive grid for mobile devices
- Hover effects for cards
- Consistent styling with existing design system
- Section placed directly after main leaderboard results
Co-authored-by: openhands <openhands@all-hands.dev>
- content.py +190 -0
- main_page.py +13 -1
- ui_components.py +156 -0
content.py
CHANGED
|
@@ -963,4 +963,194 @@ h3 .header-link-icon {
|
|
| 963 |
#main-leaderboard td:nth-child(7) .prose {
|
| 964 |
font-weight: 700 !important;
|
| 965 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 966 |
"""
|
|
|
|
| 963 |
#main-leaderboard td:nth-child(7) .prose {
|
| 964 |
font-weight: 700 !important;
|
| 965 |
}
|
| 966 |
+
|
| 967 |
+
/* ====== Winners by Category Section ====== */
|
| 968 |
+
.winners-by-category-container {
|
| 969 |
+
display: grid;
|
| 970 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 971 |
+
gap: 20px;
|
| 972 |
+
margin: 20px 0;
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
.winner-category-card {
|
| 976 |
+
background: #fff;
|
| 977 |
+
border: 1px solid #032629;
|
| 978 |
+
border-radius: 12px;
|
| 979 |
+
padding: 16px;
|
| 980 |
+
transition: box-shadow 0.2s ease;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.dark .winner-category-card {
|
| 984 |
+
background: rgba(250, 242, 233, 0.05);
|
| 985 |
+
border-color: #9fead1;
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
.winner-category-card:hover {
|
| 989 |
+
box-shadow: 0 4px 12px rgba(3, 38, 41, 0.15);
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
.dark .winner-category-card:hover {
|
| 993 |
+
box-shadow: 0 4px 12px rgba(159, 234, 209, 0.2);
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
.winner-category-header {
|
| 997 |
+
display: flex;
|
| 998 |
+
align-items: center;
|
| 999 |
+
gap: 12px;
|
| 1000 |
+
margin-bottom: 16px;
|
| 1001 |
+
padding-bottom: 12px;
|
| 1002 |
+
border-bottom: 2px solid #F0529C;
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
.dark .winner-category-header {
|
| 1006 |
+
border-bottom-color: #0FCB8C;
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
.winner-category-icon {
|
| 1010 |
+
width: 32px;
|
| 1011 |
+
height: 32px;
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
.winner-category-header h3 {
|
| 1015 |
+
margin: 0;
|
| 1016 |
+
font-size: 16px;
|
| 1017 |
+
font-weight: 700;
|
| 1018 |
+
color: #032629;
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
.dark .winner-category-header h3 {
|
| 1022 |
+
color: #fff;
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
.winner-table {
|
| 1026 |
+
width: 100%;
|
| 1027 |
+
border-collapse: collapse;
|
| 1028 |
+
font-size: 13px;
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
.winner-table thead tr {
|
| 1032 |
+
border-bottom: 1px solid #ddd;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
.dark .winner-table thead tr {
|
| 1036 |
+
border-bottom-color: #4a5a5a;
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
.winner-table th {
|
| 1040 |
+
padding: 8px 4px;
|
| 1041 |
+
text-align: left;
|
| 1042 |
+
font-weight: 600;
|
| 1043 |
+
color: #667876;
|
| 1044 |
+
font-size: 11px;
|
| 1045 |
+
text-transform: uppercase;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
.dark .winner-table th {
|
| 1049 |
+
color: #9fead1;
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
.winner-table td {
|
| 1053 |
+
padding: 10px 4px;
|
| 1054 |
+
vertical-align: middle;
|
| 1055 |
+
border-bottom: 1px solid #eee;
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
.dark .winner-table td {
|
| 1059 |
+
border-bottom-color: #2a3a3a;
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
.winner-table tbody tr:last-child td {
|
| 1063 |
+
border-bottom: none;
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
.winner-table .rank-col {
|
| 1067 |
+
width: 32px;
|
| 1068 |
+
text-align: center;
|
| 1069 |
+
font-size: 14px;
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
.winner-table .score-col {
|
| 1073 |
+
width: 60px;
|
| 1074 |
+
text-align: right;
|
| 1075 |
+
color: #F0529C;
|
| 1076 |
+
font-size: 14px;
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
.dark .winner-table .score-col {
|
| 1080 |
+
color: #0FCB8C;
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
.system-info {
|
| 1084 |
+
display: flex;
|
| 1085 |
+
align-items: center;
|
| 1086 |
+
gap: 10px;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
.company-logo-small {
|
| 1090 |
+
width: 20px;
|
| 1091 |
+
height: 20px;
|
| 1092 |
+
flex-shrink: 0;
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
.system-details {
|
| 1096 |
+
display: flex;
|
| 1097 |
+
flex-direction: column;
|
| 1098 |
+
overflow: hidden;
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
.sdk-name {
|
| 1102 |
+
font-weight: 600;
|
| 1103 |
+
color: #032629;
|
| 1104 |
+
font-size: 13px;
|
| 1105 |
+
white-space: nowrap;
|
| 1106 |
+
overflow: hidden;
|
| 1107 |
+
text-overflow: ellipsis;
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
.dark .sdk-name {
|
| 1111 |
+
color: #fff;
|
| 1112 |
+
}
|
| 1113 |
+
|
| 1114 |
+
.model-name {
|
| 1115 |
+
font-size: 11px;
|
| 1116 |
+
color: #667876;
|
| 1117 |
+
white-space: nowrap;
|
| 1118 |
+
overflow: hidden;
|
| 1119 |
+
text-overflow: ellipsis;
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
.dark .model-name {
|
| 1123 |
+
color: #9fead1;
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
.no-data {
|
| 1127 |
+
text-align: center;
|
| 1128 |
+
color: #999;
|
| 1129 |
+
font-style: italic;
|
| 1130 |
+
padding: 20px !important;
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
/* Responsive adjustments for winners section */
|
| 1134 |
+
@media (max-width: 600px) {
|
| 1135 |
+
.winners-by-category-container {
|
| 1136 |
+
grid-template-columns: 1fr;
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
.winner-category-card {
|
| 1140 |
+
padding: 12px;
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
.winner-table .score-col,
|
| 1144 |
+
.winner-table .rank-col {
|
| 1145 |
+
font-size: 12px;
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
+
.sdk-name {
|
| 1149 |
+
font-size: 12px;
|
| 1150 |
+
}
|
| 1151 |
+
|
| 1152 |
+
.model-name {
|
| 1153 |
+
font-size: 10px;
|
| 1154 |
+
}
|
| 1155 |
+
}
|
| 1156 |
"""
|
main_page.py
CHANGED
|
@@ -3,7 +3,11 @@ matplotlib.use('Agg')
|
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
|
| 6 |
-
from ui_components import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from content import (
|
| 9 |
CITATION_BUTTON_LABEL,
|
|
@@ -39,6 +43,14 @@ def build_page():
|
|
| 39 |
split_name="test"
|
| 40 |
)
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
# --- New Visualization Sections ---
|
| 43 |
gr.Markdown("---")
|
| 44 |
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
|
| 5 |
|
| 6 |
+
from ui_components import (
|
| 7 |
+
create_leaderboard_display,
|
| 8 |
+
get_full_leaderboard_data,
|
| 9 |
+
create_winners_by_category_html,
|
| 10 |
+
)
|
| 11 |
|
| 12 |
from content import (
|
| 13 |
CITATION_BUTTON_LABEL,
|
|
|
|
| 43 |
split_name="test"
|
| 44 |
)
|
| 45 |
|
| 46 |
+
# --- Winners by Category Section ---
|
| 47 |
+
gr.Markdown("---")
|
| 48 |
+
gr.HTML('<h2>Winners by Category</h2>', elem_id="winners-header")
|
| 49 |
+
gr.Markdown("Top 5 performing systems in each benchmark category.")
|
| 50 |
+
|
| 51 |
+
winners_html = create_winners_by_category_html(test_df, top_n=5)
|
| 52 |
+
gr.HTML(winners_html, elem_id="winners-by-category")
|
| 53 |
+
|
| 54 |
# --- New Visualization Sections ---
|
| 55 |
gr.Markdown("---")
|
| 56 |
|
ui_components.py
CHANGED
|
@@ -414,6 +414,162 @@ except ImportError:
|
|
| 414 |
pass # setup_data may not be available during import
|
| 415 |
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
class DummyViewer:
|
| 418 |
"""A mock viewer to be cached on error. It has a ._load() method
|
| 419 |
to ensure it behaves like the real LeaderboardViewer."""
|
|
|
|
| 414 |
pass # setup_data may not be available during import
|
| 415 |
|
| 416 |
|
| 417 |
+
# Category definitions for Winners by Category section
|
| 418 |
+
CATEGORY_DEFINITIONS = [
|
| 419 |
+
{"name": "Issue Resolution", "score_col": "Issue Resolution Score", "icon": "bug-fixing.svg"},
|
| 420 |
+
{"name": "Greenfield", "score_col": "Greenfield Score", "icon": "app-creation.svg"},
|
| 421 |
+
{"name": "Frontend", "score_col": "Frontend Score", "icon": "frontend-development.svg"},
|
| 422 |
+
{"name": "Testing", "score_col": "Testing Score", "icon": "test-generation.svg"},
|
| 423 |
+
{"name": "Information Gathering", "score_col": "Information Gathering Score", "icon": "information-gathering.svg"},
|
| 424 |
+
]
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def get_winners_by_category(df: pd.DataFrame, top_n: int = 5) -> dict:
|
| 428 |
+
"""
|
| 429 |
+
Extract the top N systems for each category based on their score.
|
| 430 |
+
|
| 431 |
+
Args:
|
| 432 |
+
df: The full leaderboard DataFrame with all scores
|
| 433 |
+
top_n: Number of top systems to return per category (default: 5)
|
| 434 |
+
|
| 435 |
+
Returns:
|
| 436 |
+
Dictionary mapping category name to list of top systems with their scores
|
| 437 |
+
"""
|
| 438 |
+
winners = {}
|
| 439 |
+
|
| 440 |
+
for cat_def in CATEGORY_DEFINITIONS:
|
| 441 |
+
cat_name = cat_def["name"]
|
| 442 |
+
score_col = cat_def["score_col"]
|
| 443 |
+
|
| 444 |
+
if score_col not in df.columns:
|
| 445 |
+
winners[cat_name] = []
|
| 446 |
+
continue
|
| 447 |
+
|
| 448 |
+
# Filter to rows that have a valid score for this category
|
| 449 |
+
cat_df = df[df[score_col].notna()].copy()
|
| 450 |
+
|
| 451 |
+
if cat_df.empty:
|
| 452 |
+
winners[cat_name] = []
|
| 453 |
+
continue
|
| 454 |
+
|
| 455 |
+
# Sort by score descending and take top N
|
| 456 |
+
cat_df = cat_df.sort_values(score_col, ascending=False).head(top_n)
|
| 457 |
+
|
| 458 |
+
# Extract relevant info
|
| 459 |
+
top_systems = []
|
| 460 |
+
for rank, (_, row) in enumerate(cat_df.iterrows(), 1):
|
| 461 |
+
system_info = {
|
| 462 |
+
"rank": rank,
|
| 463 |
+
"sdk_version": row.get("SDK Version", "Unknown"),
|
| 464 |
+
"language_model": row.get("Language Model", "Unknown"),
|
| 465 |
+
"score": row[score_col],
|
| 466 |
+
}
|
| 467 |
+
top_systems.append(system_info)
|
| 468 |
+
|
| 469 |
+
winners[cat_name] = top_systems
|
| 470 |
+
|
| 471 |
+
return winners
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
def create_winners_by_category_html(df: pd.DataFrame, top_n: int = 5) -> str:
|
| 475 |
+
"""
|
| 476 |
+
Create an HTML table displaying the top N winners for each category.
|
| 477 |
+
|
| 478 |
+
Args:
|
| 479 |
+
df: The full leaderboard DataFrame
|
| 480 |
+
top_n: Number of top systems to show per category
|
| 481 |
+
|
| 482 |
+
Returns:
|
| 483 |
+
HTML string with the winners table
|
| 484 |
+
"""
|
| 485 |
+
winners = get_winners_by_category(df, top_n)
|
| 486 |
+
|
| 487 |
+
# Build the HTML table
|
| 488 |
+
html_parts = ['<div class="winners-by-category-container">']
|
| 489 |
+
|
| 490 |
+
# Create a card for each category
|
| 491 |
+
for cat_def in CATEGORY_DEFINITIONS:
|
| 492 |
+
cat_name = cat_def["name"]
|
| 493 |
+
icon_file = cat_def["icon"]
|
| 494 |
+
icon_uri = get_svg_as_data_uri(f"assets/{icon_file}")
|
| 495 |
+
|
| 496 |
+
top_systems = winners.get(cat_name, [])
|
| 497 |
+
|
| 498 |
+
html_parts.append(f'''
|
| 499 |
+
<div class="winner-category-card">
|
| 500 |
+
<div class="winner-category-header">
|
| 501 |
+
<img src="{icon_uri}" alt="{cat_name}" class="winner-category-icon">
|
| 502 |
+
<h3>{cat_name}</h3>
|
| 503 |
+
</div>
|
| 504 |
+
<table class="winner-table">
|
| 505 |
+
<thead>
|
| 506 |
+
<tr>
|
| 507 |
+
<th class="rank-col">#</th>
|
| 508 |
+
<th class="system-col">System</th>
|
| 509 |
+
<th class="score-col">Score</th>
|
| 510 |
+
</tr>
|
| 511 |
+
</thead>
|
| 512 |
+
<tbody>
|
| 513 |
+
''')
|
| 514 |
+
|
| 515 |
+
if top_systems:
|
| 516 |
+
for system in top_systems:
|
| 517 |
+
rank = system["rank"]
|
| 518 |
+
sdk_version = system["sdk_version"]
|
| 519 |
+
language_model = system["language_model"]
|
| 520 |
+
score = system["score"]
|
| 521 |
+
|
| 522 |
+
# Get company logo for the model
|
| 523 |
+
company_info = get_company_from_model(language_model)
|
| 524 |
+
logo_uri = get_svg_as_data_uri(company_info["path"])
|
| 525 |
+
|
| 526 |
+
# Format model name - clean it if it's a list
|
| 527 |
+
if isinstance(language_model, list):
|
| 528 |
+
language_model = language_model[0] if language_model else "Unknown"
|
| 529 |
+
model_display = str(language_model).split('/')[-1] # Remove provider prefix
|
| 530 |
+
|
| 531 |
+
# Add medal emoji for top 3
|
| 532 |
+
rank_display = rank
|
| 533 |
+
if rank == 1:
|
| 534 |
+
rank_display = "🥇"
|
| 535 |
+
elif rank == 2:
|
| 536 |
+
rank_display = "🥈"
|
| 537 |
+
elif rank == 3:
|
| 538 |
+
rank_display = "🥉"
|
| 539 |
+
|
| 540 |
+
html_parts.append(f'''
|
| 541 |
+
<tr>
|
| 542 |
+
<td class="rank-col">{rank_display}</td>
|
| 543 |
+
<td class="system-col">
|
| 544 |
+
<div class="system-info">
|
| 545 |
+
<img src="{logo_uri}" alt="{company_info['name']}" class="company-logo-small">
|
| 546 |
+
<div class="system-details">
|
| 547 |
+
<span class="sdk-name">{sdk_version}</span>
|
| 548 |
+
<span class="model-name">{model_display}</span>
|
| 549 |
+
</div>
|
| 550 |
+
</div>
|
| 551 |
+
</td>
|
| 552 |
+
<td class="score-col"><strong>{score:.1f}</strong></td>
|
| 553 |
+
</tr>
|
| 554 |
+
''')
|
| 555 |
+
else:
|
| 556 |
+
html_parts.append('''
|
| 557 |
+
<tr>
|
| 558 |
+
<td colspan="3" class="no-data">No submissions yet</td>
|
| 559 |
+
</tr>
|
| 560 |
+
''')
|
| 561 |
+
|
| 562 |
+
html_parts.append('''
|
| 563 |
+
</tbody>
|
| 564 |
+
</table>
|
| 565 |
+
</div>
|
| 566 |
+
''')
|
| 567 |
+
|
| 568 |
+
html_parts.append('</div>')
|
| 569 |
+
|
| 570 |
+
return ''.join(html_parts)
|
| 571 |
+
|
| 572 |
+
|
| 573 |
class DummyViewer:
|
| 574 |
"""A mock viewer to be cached on error. It has a ._load() method
|
| 575 |
to ensure it behaves like the real LeaderboardViewer."""
|