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()