from pathlib import Path
from typing import Optional
import gradio as gr
from gradio_leaderboard import ColumnFilter, Leaderboard, SelectColumns
import pandas as pd
import re
def _slugify(title: str) -> str:
return re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')
# 🎨 增强后的自定义 CSS
custom_css = """
/* 全局设置:简洁、高级的字体和背景 */
:root {
--color-background-primary: #f8f8f8; /* 浅米白色背景 */
--color-background-secondary: #ffffff; /* 卡片背景 */
--color-text-primary: #333333;
--color-accent: #8e80ff; /* 浅紫色强调色 (Primary) */
--color-accent-light: #a99dff; /* 浅紫色悬停色 */
--shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.08);
}
/* 全局字体:强制使用 Arial */
html, body, .gradio-container, .gradio-container * {
font-family: Arial, "Helvetica Neue", Helvetica, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif !important;
}
body {
background-color: var(--color-background-primary) !important;
}
/* 增加容器最大宽度以展示完整表格 */
.gradio-container {
max-width: 1400px; /* 宽度从 1800px 调窄到 1400px */
margin: 0 auto;
padding: 20px;
}
/* 标题样式 */
#space-title {
color: var(--color-text-primary);
font-size: 3em;
font-weight: 700;
margin-bottom: 0.5em;
padding-top: 20px;
}
/* Group/Block 组件的卡片样式 */
.gr-group, .gr-block {
background-color: var(--color-background-secondary);
border-radius: 12px;
box-shadow: var(--shadow-medium);
transition: box-shadow 0.3s ease;
padding: 15px;
margin-bottom: 20px;
}
.gr-group:hover, .gr-block:hover {
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
}
/* Leaderboard 容器:调整内部布局的关键 */
[id^="leaderboard-"] {
padding: 0 !important;
}
/* 搜索栏布局调整 (第一行) */
.leaderboard_root > div:nth-child(1) {
padding: 0 15px 15px 15px;
}
/* 过滤器和列选择布局调整 (第二行) */
.leaderboard_root > div:nth-child(2) {
display: flex;
padding: 0 15px 15px 15px;
}
.leaderboard_root .gr-form {
border: none;
}
/* Search Bar */
#search-bar-table-box {
width: 100%;
margin-bottom: 10px;
}
#search-bar-table-box > div:first-child {
background: none;
border: none;
}
/* === Select Columns to Display: 强制单行展示 === */
/* 定位 SelectColumns 的内部复选框容器 */
.leaderboard-filter-column:first-child .gr-form-checkbox-group {
/* 使用 flex 容器 */
display: flex !important;
flex-wrap: nowrap !important; /* 强制不换行 */
overflow-x: auto !important; /* 允许水平滚动 */
gap: 10px;
padding-bottom: 5px;
}
/* 确保每个复选框标签保持内联块级元素 */
.leaderboard-filter-column:first-child .gr-form-checkbox-group label {
flex-shrink: 0 !important; /* 防止选项被压缩 */
display: inline-block !important; /* 确保每个选项占据其自然宽度 */
margin: 0;
white-space: nowrap; /* 确保文字也不换行 */
}
#leaderboard-table, #leaderboard-table-lite {
margin-top: 15px;
border-radius: 8px;
overflow: hidden;
}
#leaderboard-table th {
background-color: var(--color-accent);
color: white;
font-weight: 600;
text-transform: uppercase;
border-bottom: 2px solid var(--color-accent-light);
}
#leaderboard-table tr:hover {
background-color: #f0f0f0;
cursor: pointer;
transition: background-color 0.2s ease;
}
#leaderboard-table td:nth-child(2),
#leaderboard-table th:nth-child(2) {
max-width: 400px;
overflow: auto;
white-space: nowrap;
}
#leaderboard-table td:nth-child(3) {
font-weight: bold;
color: var(--color-accent);
}
/* Citation 区域 */
#citation-group {
padding: 20px;
margin-top: 10px;
}
#citation-button {
margin-top: 0;
padding: 0;
}
/* 修复 Citation 复制图标重叠问题 */
#citation-button label {
display: block;
position: relative;
}
#citation-button textarea {
font-family: Arial, "Helvetica Neue", Helvetica, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif !important;
background-color: #f1f1f1;
border: 1px solid #cccccc;
border-radius: 6px;
padding: 10px;
padding-right: 40px !important; /* 为复制按钮腾出空间 */
font-size: 14px !important;
width: 100% !important;
box-sizing: border-box;
}
/* 调整复制按钮的位置 */
#citation-button > label > button {
position: absolute;
top: 10px;
right: 10px;
margin: 0;
transform: scale(1.1);
transition: transform 0.2s ease;
background-color: var(--color-accent) !important;
color: white !important;
border: none !important;
border-radius: 6px;
z-index: 10;
}
#citation-button > label > button:hover {
transform: scale(1.2);
background-color: var(--color-accent-light) !important;
}
/* Leaderboard 内部过滤/选择组件微调 */
.leaderboard_root .leaderboard-filter-column:last-child {
flex-grow: 1;
max-width: 50%;
}
.leaderboard_root .leaderboard-filter-column:first-child {
max-width: 50%;
padding-right: 20px;
}
/* 其他 Gradio 元素的简洁化 */
.wrap-inner input[type="text"], .wrap-inner input[type="number"] {
border-radius: 6px;
border: 1px solid #cccccc;
padding: 8px 12px;
}
/* ==== Score bar cells ==== */
.leaderboard-cell-bar {
position: relative;
display: block;
width: 100%;
height: 28px;
line-height: 28px;
background: #f5f3ff; /* light purple background */
border-radius: 8px;
overflow: hidden;
padding-left: 38px; /* leave room for dot */
color: #1d1b84; /* dark purple text */
font-weight: 600;
}
.leaderboard-cell-bar .bar-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: var(--w, 0%);
background: linear-gradient(90deg, #6c5ce7 0%, #a29bfe 100%);
opacity: 0.25;
}
.leaderboard-cell-bar .bar-dot {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
border-radius: 50%;
background: #3c1be3;
box-shadow: 0 0 0 4px rgba(60, 27, 227, 0.08);
}
.leaderboard-cell-bar .bar-text {
position: relative;
z-index: 1;
padding-right: 10px;
}
"""
TITLE = """
SciEval Leaderboards 🏆
"""
INFO = """
HuggingFace ·
GitHub
"""
CITATION_BUTTON_LABEL = "📖 Citation"
CITATION_BUTTON_TEXT = r"""
@article{scieval2025,
title={SciEvalKit: An Open-source Evaluation Toolkit for Scientific General Intelligence},
author={SciPrismaX Team},
journal={arXiv preprint},
year={2025}
}
"""
LEADERBOARD_FILES = [
("Large Language Model Scientific Capability", "Large Language Model Scientific Capability.csv"),
("Multimodal Model Scientific Capability", "Multimodal Model Scientific Capability.csv"),
("Multimodal Model Disciplinary Leaderboard", "Multimodal Model Disciplinary Leaderboard.csv"),
]
def strip_auxiliary_columns(df: pd.DataFrame) -> pd.DataFrame:
"""Remove unnamed columns that come from spreadsheet index exports."""
return df.loc[:, ~df.columns.str.contains("^Unnamed")]
def find_sort_column(df: pd.DataFrame) -> Optional[str]:
"""Pick a sensible default sort column."""
preferred = ["overall", "score", "avg", "average"]
for col in df.columns:
if col.lower() in preferred and pd.api.types.is_numeric_dtype(df[col]):
return col
numeric_cols = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col])]
return numeric_cols[0] if numeric_cols else None
def _percent_widths(series: pd.Series) -> pd.Series:
"""Compute a 0-100 width for a numeric series."""
s = series.astype(float)
# If values look like percentages already
if s.min() >= 0 and s.max() <= 100:
return s
# If values look like 0-1
if s.min() >= 0 and s.max() <= 1.0:
return s * 100.0
# General min-max scaling
rng = s.max() - s.min()
if rng == 0:
return pd.Series([50.0] * len(s), index=s.index)
return (s - s.min()) / rng * 100.0
def add_bar_cells(df: pd.DataFrame, exclude: Optional[list[str]] = None) -> tuple[pd.DataFrame, set[str]]:
"""
Convert numeric score columns to HTML with a bar background.
Returns a new DataFrame and the set of columns that were converted.
"""
exclude = set((exclude or []))
# Columns we never bar-render
exclude |= {"Model", "Type", "Parameters"}
out = df.copy()
converted: set[str] = set()
for col in out.columns:
if col in exclude:
continue
if pd.api.types.is_numeric_dtype(out[col]):
widths = _percent_widths(out[col])
# Build HTML for each cell
formatted = []
for val, w in zip(out[col], widths):
try:
disp = f"{float(val):.2f}"
except Exception:
disp = str(val)
html = (
f''
f''
f''
f'{disp}'
f"
"
)
formatted.append(html)
out[col] = formatted
converted.add(col)
return out, converted
def load_leaderboard_csv(path: Path) -> pd.DataFrame:
"""Read and clean a leaderboard CSV."""
df = pd.read_csv(path)
df = strip_auxiliary_columns(df)
df.columns = [col.strip() for col in df.columns]
numeric_cols = [col for col in df.columns if pd.api.types.is_numeric_dtype(df[col])]
if numeric_cols:
df[numeric_cols] = df[numeric_cols].round(2)
sort_col = find_sort_column(df)
if sort_col:
df = df.sort_values(by=sort_col, ascending=False)
return df.reset_index(drop=True)
def safe_load(title: str, path: Path) -> tuple[str, pd.DataFrame]:
"""Load a leaderboard but keep the app running if the CSV is missing or malformed."""
try:
df = load_leaderboard_csv(path)
except Exception as exc:
print(f"[leaderboard] Failed to load {path}: {exc}")
df = pd.DataFrame(
{
"Status": [
f"Upload a CSV named '{path.name}' to populate the '{title}' leaderboard. "
f"Error: {exc}"
]
}
)
return title, df
def build_datatypes(df: pd.DataFrame, html_cols: Optional[set[str]] = None) -> list[str]:
"""Build the datatype list for gradio_leaderboard.
Columns we bar-render should be treated as markdown so inline HTML is rendered.
"""
html_cols = html_cols or set()
dtypes: list[str] = []
for col in df.columns:
if col in html_cols:
# Use markdown to allow raw HTML inside cells
dtypes.append("markdown")
else:
dtypes.append("number" if pd.api.types.is_numeric_dtype(df[col]) else "str")
return dtypes
def discover_leaderboards(config: list[tuple[str, str]]) -> list[tuple[str, pd.DataFrame]]:
"""Load configured leaderboards; if a file is renamed, fall back to any other CSVs in the folder."""
configured_paths = [(title, Path(filename)) for title, filename in config]
configured_names = {Path(filename).name for _, filename in config}
# Load explicitly configured CSVs first
boards: list[tuple[str, pd.DataFrame]] = [safe_load(title, path) for title, path in configured_paths]
# Add any other CSVs in the folder as additional tabs for resilience
extra_csvs = [
path
for path in sorted(Path(".").glob("*.csv"))
if path.name not in configured_names
]
for path in extra_csvs:
boards.append(safe_load(path.stem, path))
return boards
leaderboards = discover_leaderboards(LEADERBOARD_FILES)
required_filenames_md = "\n".join([f" - `{filename}`" for _, filename in LEADERBOARD_FILES])
demo = gr.Blocks(css=custom_css, theme=gr.themes.Soft())
with demo:
gr.HTML(TITLE)
gr.HTML(INFO)
# Render independent leaderboards (no tabs)
for lb_title, df in leaderboards:
with gr.Group():
centered_titles = {
"Large Language Model Scientific Capability",
"Multimodal Model Scientific Capability",
"Multimodal Model Disciplinary Leaderboard",
}
if lb_title.strip() in centered_titles:
gr.HTML(f'{lb_title}
')
else:
gr.Markdown(f"## {lb_title}")
# Apply bar-style rendering to numeric score columns
df_render, html_cols = add_bar_cells(df)
Leaderboard(
value=df_render,
elem_id=f"leaderboard-{_slugify(lb_title)}",
datatype=build_datatypes(df_render, html_cols),
select_columns=SelectColumns(
default_selection=list(df_render.columns),
cant_deselect=[c for c in ("Model", "Type") if c in df_render.columns],
label="Select columns to display:",
),
search_columns=["Model"] if "Model" in df_render.columns else [df_render.columns[0]],
filter_columns=(
[ColumnFilter("Type", type="checkboxgroup", label="Model Types:")]
if "Type" in df_render.columns else []
),
interactive=False,
)
gr.Markdown("---")
with gr.Row():
with gr.Column():
with gr.Group(elem_id="citation-group"):
gr.Textbox(
value=CITATION_BUTTON_TEXT,
label=CITATION_BUTTON_LABEL,
lines=CITATION_BUTTON_TEXT.count("\n") + 1,
elem_id="citation-button",
show_copy_button=True,
interactive=False,
)
demo.queue(default_concurrency_limit=40).launch()