Spaces:
Running
Running
Download src/ui.py from ai-forever/WebPageBench: direct link, hf CLI and curl.
- Browser
- Download file 32.2 kB
-
https://huggingface.co/spaces/ai-forever/WebPageBench/resolve/main/src/ui.py
- Command line
-
hf download hf://spaces/ai-forever/WebPageBench/src/ui.py
-
curl -L -o ui.py https://huggingface.co/spaces/ai-forever/WebPageBench/resolve/main/src/ui.py
32.2 kB
| """HTML UI for the WebPageBench leaderboard.""" | |
| from __future__ import annotations | |
| import html | |
| from pathlib import Path | |
| from typing import Any | |
| from src.bench_config import load_bench_config, task_count | |
| from src.formatting import fmt_cost, fmt_duration, fmt_number, fmt_pct_value, fmt_steps, round2 | |
| from src.i18n import get_lang, section_desc_t, section_label_t, t, taxonomy_desc_t | |
| from src.load_entries import sort_entries | |
| from src.models import LeaderboardEntry | |
| from src.token_display import fmt_tokens, format_model_usage_breakdown_html | |
| _DETAIL_TAB_LABELS = { | |
| "submission": "Details", | |
| "sections": "Sections", | |
| "taxonomy": "UI taxonomy", | |
| "metrics": "Metrics", | |
| } | |
| def load_css() -> str: | |
| return (Path(__file__).resolve().parent.parent / "assets" / "webpagebench.css").read_text(encoding="utf-8") | |
| def score_tier(pct: float | None) -> str: | |
| if pct is None: | |
| return "danger" | |
| if pct >= 70: | |
| return "success" | |
| if pct >= 40: | |
| return "warning" | |
| return "danger" | |
| def fmt_pct(value: float | None) -> str: | |
| return fmt_pct_value(value) | |
| def fmt_pct_display(entry: LeaderboardEntry) -> str: | |
| if entry.success_rate is None: | |
| return "—" | |
| return fmt_pct_value(entry.success_rate) | |
| def provider_from_model(model: str, provider: str | None = None) -> str: | |
| if provider: | |
| return provider | |
| if "/" in model: | |
| return model.split("/", 1)[0] | |
| return model.split("-", 1)[0] | |
| def _badge_link(label: str, passed: object, total: object, css_class: str, title: str, nav: str) -> str: | |
| stat_html = "" | |
| try: | |
| passed_i = int(passed) if passed is not None else None | |
| total_i = int(total) if total is not None else None | |
| except (TypeError, ValueError): | |
| passed_i = total_i = None | |
| if passed_i is not None and total_i is not None: | |
| stat_html = f'<span class="wab-badge-stat">{passed_i}/{total_i}</span>' | |
| return ( | |
| f'<span role="button" tabindex="0" class="wab-badge wab-badge-link {css_class}" ' | |
| f'title="{html.escape(title)}" data-wab-nav="{html.escape(nav)}">' | |
| f"{html.escape(label)}{stat_html}</span>" | |
| ) | |
| def _badge(label: str, css_class: str, title: str) -> str: | |
| return ( | |
| f'<span class="wab-badge {css_class}" title="{html.escape(title)}">' | |
| f"{html.escape(label)}</span>" | |
| ) | |
| def _row_badges(entry: LeaderboardEntry) -> str: | |
| config = load_bench_config() | |
| badges: list[str] = [] | |
| ranked_sections = sorted( | |
| ( | |
| (stats.success_rate, stats.section_id, section_label_t(stats.section_id), stats.passed, stats.total) | |
| for stats in entry.sections.values() | |
| if stats.success_rate is not None | |
| ), | |
| reverse=True, | |
| )[:2] | |
| for _, section_id, label, passed, total in ranked_sections: | |
| desc = section_desc_t(section_id) or label | |
| nav = f"sections:{section_id}" | |
| badges.append(_badge_link(label, passed, total, "domain", str(desc), nav)) | |
| ranked_taxonomy = sorted( | |
| ( | |
| (float(row.get("success_rate")), class_id) | |
| for class_id, row in entry.ui_classes.items() | |
| if isinstance(row.get("success_rate"), (int, float)) | |
| ), | |
| reverse=True, | |
| )[:2] | |
| if not ranked_taxonomy and entry.ui_badges: | |
| for class_id in entry.ui_badges[:2]: | |
| meta = config.get("ui_classes", {}).get(class_id, {}) | |
| label = str(meta.get("label") or class_id) | |
| row = entry.ui_classes.get(class_id) or {} | |
| nav = f"taxonomy:{class_id}" | |
| badges.append( | |
| _badge_link( | |
| label, | |
| row.get("passed"), | |
| row.get("total"), | |
| "taxonomy", | |
| taxonomy_desc_t(class_id) or str(meta.get("description") or class_id), | |
| nav, | |
| ) | |
| ) | |
| else: | |
| for _, class_id in ranked_taxonomy: | |
| meta = config.get("ui_classes", {}).get(class_id, {}) | |
| row = entry.ui_classes.get(class_id) or {} | |
| label = str(meta.get("label") or class_id) | |
| nav = f"taxonomy:{class_id}" | |
| badges.append( | |
| _badge_link( | |
| label, | |
| row.get("passed"), | |
| row.get("total"), | |
| "taxonomy", | |
| taxonomy_desc_t(class_id) or str(meta.get("description") or class_id), | |
| nav, | |
| ) | |
| ) | |
| return "".join(badges) | |
| def top_section_badges(entry: LeaderboardEntry, limit: int = 2) -> list[str]: | |
| ranked = [ | |
| (stats.success_rate, section_label_t(stats.section_id)) | |
| for stats in entry.sections.values() | |
| if stats.success_rate is not None | |
| ] | |
| ranked.sort(reverse=True) | |
| return [label for _, label in ranked[:limit]] | |
| def pick_winner(entries: list[LeaderboardEntry], metric: str, *, ascending: bool = False) -> LeaderboardEntry | None: | |
| if not entries: | |
| return None | |
| values = [(entry, entry.metric(metric)) for entry in entries] | |
| values = [(entry, value) for entry, value in values if value is not None] | |
| if not values: | |
| return None | |
| return min(values, key=lambda pair: pair[1])[0] if ascending else max(values, key=lambda pair: pair[1])[0] | |
| def _quick_card(label: str, headline: str, entry: LeaderboardEntry, subtitle: str) -> str: | |
| return f""" | |
| <article class="wab-quick-card"> | |
| <div class="wab-quick-kicker">{html.escape(label)}</div> | |
| <div class="wab-quick-score">{html.escape(headline)}</div> | |
| <div class="wab-quick-model">{html.escape(entry.model)}</div> | |
| <div class="wab-quick-meta">{html.escape(subtitle)}<br>{html.escape(entry.harness)}</div> | |
| </article> | |
| """ | |
| def _pick_headline(entry: LeaderboardEntry, metric: str) -> str: | |
| if metric == "success_rate" or metric == "full_bench_success_rate": | |
| return fmt_pct_display(entry) | |
| if metric == "avg_duration_seconds": | |
| return fmt_duration(entry.avg_duration_seconds) | |
| if metric == "total_duration_seconds": | |
| return fmt_duration(entry.total_duration_seconds) | |
| if metric in {"total_cost_usd", "avg_cost_per_task_usd"}: | |
| return fmt_cost(entry.total_cost_usd) | |
| if metric == "value_score": | |
| value = entry.value_score | |
| return fmt_number(value) if value is not None else "—" | |
| if metric.startswith("domain:"): | |
| return fmt_pct(entry.metric(metric)) | |
| return fmt_pct_display(entry) | |
| _GLOBAL_HIGHLIGHTS: tuple[dict[str, object], ...] = ( | |
| { | |
| "label": "best_overall", | |
| "metric": "success_rate", | |
| "ascending": False, | |
| "subtitle": "best_overall_sub", | |
| }, | |
| { | |
| "label": "best_budget", | |
| "metric": "total_cost_usd", | |
| "ascending": True, | |
| "subtitle": "best_budget_sub", | |
| }, | |
| { | |
| "label": "fastest", | |
| "metric": "total_duration_seconds", | |
| "ascending": True, | |
| "subtitle": "fastest_sub", | |
| }, | |
| ) | |
| def build_highlight_picks(entries: list[LeaderboardEntry]) -> str: | |
| """Top highlights shown above view tabs on every leaderboard page.""" | |
| cards: list[str] = [] | |
| for pick in _GLOBAL_HIGHLIGHTS: | |
| metric = str(pick["metric"]) | |
| winner = pick_winner(entries, metric, ascending=bool(pick["ascending"])) | |
| if winner is None: | |
| continue | |
| cards.append( | |
| _quick_card( | |
| t(str(pick["label"])), | |
| _pick_headline(winner, metric), | |
| winner, | |
| t(str(pick["subtitle"])), | |
| ) | |
| ) | |
| if not cards: | |
| return "" | |
| return ( | |
| '<section class="wab-highlights">' | |
| f'<div class="wab-quick-grid">{"".join(cards)}</div>' | |
| "</section>" | |
| ) | |
| def build_quick_picks(entries: list[LeaderboardEntry], view_id: str) -> str: | |
| config = load_bench_config() | |
| picks = config["quick_picks"].get(view_id, []) | |
| cards: list[str] = [] | |
| for pick in picks: | |
| metric = pick["metric"] | |
| ascending = metric in {"avg_duration_seconds", "total_duration_seconds", "total_cost_usd"} | |
| winner = pick_winner(entries, metric, ascending=ascending) | |
| if winner is None: | |
| continue | |
| cards.append(_quick_card(pick["label"], _pick_headline(winner, metric), winner, pick["subtitle"])) | |
| if not cards: | |
| return "" | |
| return f'<div class="wab-quick-grid">{"".join(cards)}</div>' | |
| def _input_badge(entry: LeaderboardEntry) -> str: | |
| modality = entry.input_modality | |
| css = modality.replace("+", "-plus-") if modality not in {"—", ""} else "unknown" | |
| return ( | |
| f'<span class="wab-input wab-input-{html.escape(css)}" ' | |
| f'title="{html.escape(t("input_help"))}">{html.escape(modality)}</span>' | |
| ) | |
| def _strict_cell(entry: LeaderboardEntry) -> str: | |
| if entry.strict_passed_tasks is None or entry.total_tasks <= 0: | |
| return "—" | |
| pct = entry.strict_success_pct or 0 | |
| tier = score_tier(pct) | |
| return ( | |
| f'<div class="wab-score-value wab-score-{tier}">{html.escape(fmt_pct_value(entry.strict_success_rate))}</div>' | |
| f'<div class="wab-strict-count">{entry.strict_passed_tasks}/{entry.total_tasks}</div>' | |
| ) | |
| def _sort_value(value: float | None) -> str: | |
| if value is None: | |
| return "" | |
| return f"{value:.6f}" | |
| def _sort_th(label: str, key: str, *, active: bool = False, title: str = "") -> str: | |
| active_cls = " is-active" if active else "" | |
| direction = ' data-wab-dir="desc"' if active else "" | |
| title_attr = f' title="{html.escape(title)}"' if title else "" | |
| return ( | |
| f"<th>" | |
| f'<button type="button" class="wab-sort{active_cls}" data-wab-sort="{key}"{direction}{title_attr}>' | |
| f"{html.escape(label)}</button></th>" | |
| ) | |
| def _success_row(idx: int, entry: LeaderboardEntry) -> str: | |
| pct = entry.success_pct or 0 | |
| tier = score_tier(pct) | |
| badges = _row_badges(entry) | |
| nav = html.escape(f"submission:{entry.entry_id}") | |
| tasks_ratio = entry.passed_tasks / entry.total_tasks if entry.total_tasks else None | |
| return f""" | |
| <tr class="wab-row" data-wab-nav="{nav}" data-wab-tasks="{_sort_value(tasks_ratio)}" data-wab-success="{_sort_value(entry.success_rate)}" data-wab-strict="{_sort_value(entry.strict_success_rate)}"> | |
| <td class="wab-rank">{idx + 1}</td> | |
| <td class="wab-model-cell"> | |
| <div class="wab-model-name">{html.escape(entry.model)}</div> | |
| <div class="wab-badges">{badges}</div> | |
| </td> | |
| <td><span class="wab-harness">{html.escape(entry.harness)}</span></td> | |
| <td>{_input_badge(entry)}</td> | |
| <td>{entry.passed_tasks}/{entry.total_tasks}</td> | |
| <td class="wab-score-cell"> | |
| <div class="wab-score-value wab-score-{tier}">{html.escape(fmt_pct_display(entry))}</div> | |
| <div class="wab-bar"><span class="wab-bar-{tier}" style="width:{max(pct, 0):.1f}%"></span></div> | |
| </td> | |
| <td class="wab-score-cell" title="{html.escape(t("done_pass_title"))}">{_strict_cell(entry)}</td> | |
| </tr> | |
| """ | |
| def _speed_row(idx: int, entry: LeaderboardEntry) -> str: | |
| pct = entry.success_pct or 0 | |
| tier = score_tier(pct) | |
| nav = html.escape(f"submission:{entry.entry_id}") | |
| return f""" | |
| <tr class="wab-row" data-wab-nav="{nav}"> | |
| <td class="wab-rank">{idx + 1}</td> | |
| <td class="wab-model-cell"><div class="wab-model-name">{html.escape(entry.model)}</div> | |
| <div class="wab-badges"><span class="wab-badge">{html.escape(entry.harness)}</span>{_input_badge(entry)}</div></td> | |
| <td>{html.escape(fmt_duration(entry.total_duration_seconds))}</td> | |
| <td>{html.escape(fmt_duration(entry.avg_duration_seconds))}</td> | |
| <td>{html.escape(fmt_steps(entry.total_agent_steps))}</td> | |
| <td class="wab-score-cell"> | |
| <div class="wab-score-value wab-score-{tier}">{html.escape(fmt_pct_display(entry))}</div> | |
| <div class="wab-bar"><span class="wab-bar-{tier}" style="width:{max(pct, 0):.1f}%"></span></div> | |
| </td> | |
| </tr> | |
| """ | |
| def _cost_row(idx: int, entry: LeaderboardEntry) -> str: | |
| pct = entry.success_pct or 0 | |
| tier = score_tier(pct) | |
| value = entry.value_score | |
| nav = html.escape(f"submission:{entry.entry_id}") | |
| return f""" | |
| <tr class="wab-row" data-wab-nav="{nav}"> | |
| <td class="wab-rank">{idx + 1}</td> | |
| <td class="wab-model-cell"><div class="wab-model-name">{html.escape(entry.model)}</div> | |
| <div class="wab-badges"><span class="wab-badge">{html.escape(entry.harness)}</span>{_input_badge(entry)}</div></td> | |
| <td>{html.escape(fmt_cost(entry.total_cost_usd))}</td> | |
| <td>{html.escape(fmt_cost(entry.avg_cost_per_task_usd))}</td> | |
| <td>{html.escape(fmt_tokens(entry.total_tokens))}</td> | |
| <td>{html.escape(fmt_tokens(entry.avg_tokens_per_task))}</td> | |
| <td class="wab-token-split-cell">{format_model_usage_breakdown_html(entry)}</td> | |
| <td>{fmt_number(value) if value is not None else "—"}</td> | |
| <td class="wab-score-cell"> | |
| <div class="wab-score-value wab-score-{tier}">{html.escape(fmt_pct_display(entry))}</div> | |
| <div class="wab-bar"><span class="wab-bar-{tier}" style="width:{max(pct, 0):.1f}%"></span></div> | |
| </td> | |
| </tr> | |
| """ | |
| def build_table(entries: list[LeaderboardEntry], view_id: str) -> str: | |
| sorted_entries = sort_entries(entries, view_id) | |
| if view_id == "speed": | |
| head = ( | |
| f"<tr><th>#</th><th>{html.escape(t('model'))}</th><th>{html.escape(t('total_time'))}</th>" | |
| f"<th>{html.escape(t('avg_time'))}</th><th>{html.escape(t('total_steps'))}</th>" | |
| f"<th>{html.escape(t('success'))}</th></tr>" | |
| ) | |
| rows = [_speed_row(i, entry) for i, entry in enumerate(sorted_entries)] | |
| subtitle = t("speed_sub") | |
| view_label = t("view_speed") | |
| elif view_id == "cost": | |
| head = ( | |
| f"<tr><th>#</th><th>{html.escape(t('model'))}</th><th>{html.escape(t('total_cost'))}</th>" | |
| f"<th>{html.escape(t('per_task'))}</th><th>{html.escape(t('total_tokens'))}</th>" | |
| f"<th>{html.escape(t('tokens_per_task'))}</th><th>{html.escape(t('model_usage'))}</th>" | |
| f"<th>{html.escape(t('value'))}</th><th>{html.escape(t('success'))}</th></tr>" | |
| ) | |
| rows = [_cost_row(i, entry) for i, entry in enumerate(sorted_entries)] | |
| subtitle = t("cost_sub") | |
| view_label = t("view_cost") | |
| else: | |
| head = ( | |
| f"<tr><th>#</th><th>{html.escape(t('model'))}</th><th>{html.escape(t('harness'))}</th>" | |
| f"<th>{html.escape(t('input'))}</th>" | |
| f"{_sort_th(t('tasks_col'), 'tasks')}" | |
| f"{_sort_th(t('success'), 'success', active=True)}" | |
| f"{_sort_th(t('done_pass'), 'strict', title=t('done_pass_title'))}</tr>" | |
| ) | |
| rows = [_success_row(i, entry) for i, entry in enumerate(sorted_entries)] | |
| subtitle = t("success_sub") | |
| view_label = t("view_success") | |
| body = ( | |
| "".join(rows) | |
| if rows | |
| else f'<tr><td colspan="9">{html.escape(t("no_runs_filter"))}</td></tr>' | |
| ) | |
| return f""" | |
| <div class="wab-panel"> | |
| <div class="wab-panel-head"> | |
| <h3>{html.escape(t("by_model", label=view_label))}</h3> | |
| <p>{html.escape(subtitle)}</p> | |
| </div> | |
| <div class="wab-table-wrap"> | |
| <table class="wab-table"><thead>{head}</thead><tbody>{body}</tbody></table> | |
| </div> | |
| </div> | |
| """ | |
| _GITHUB_URL = "https://github.com/ai-forever/WebPageBench" | |
| _GITHUB_ICON = ( | |
| '<svg viewBox="0 0 16 16" width="20" height="20" aria-hidden="true">' | |
| '<path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38' | |
| " 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53" | |
| ".63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95" | |
| " 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09" | |
| " 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65" | |
| " 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" | |
| '"/></svg>' | |
| ) | |
| def build_shell_top(entries: list[LeaderboardEntry]) -> str: | |
| """Header with page menu and GitHub link. View switcher is a separate Gradio control.""" | |
| config = load_bench_config() | |
| benchmark = config["benchmark"] | |
| lang = get_lang() | |
| en_active = " active" if lang != "ru" else "" | |
| ru_active = " active" if lang == "ru" else "" | |
| return f""" | |
| <div class="wab-shell" data-wab-current-lang="{html.escape(lang)}"> | |
| <div class="wab-topbar"> | |
| <div class="wab-title-row"> | |
| <div class="wab-brand"> | |
| <div> | |
| <h1>{html.escape(benchmark["name"])}</h1> | |
| <p>{html.escape(t("tagline"))}</p> | |
| </div> | |
| </div> | |
| <nav class="wab-page-nav" aria-label="{html.escape(t("pages"))}"> | |
| <button type="button" class="wab-page-link active" data-wab-page="Leaderboard">Leaderboard</button> | |
| <button type="button" class="wab-page-link" data-wab-page="About">About</button> | |
| </nav> | |
| </div> | |
| <div class="wab-top-right"> | |
| <div class="wab-stats"> | |
| <div class="wab-stat"><strong>{len(entries)}</strong> {html.escape(t("runs"))}</div> | |
| <div class="wab-stat"><strong>{task_count()}</strong> {html.escape(t("tasks"))}</div> | |
| <div class="wab-stat"><strong>{len(config["sections"])}</strong> {html.escape(t("sections_n"))}</div> | |
| </div> | |
| <div class="wab-lang" role="group" aria-label="{html.escape(t("language"))}"> | |
| <button type="button" class="wab-lang-btn{en_active}" data-wab-lang="en">EN</button> | |
| <button type="button" class="wab-lang-btn{ru_active}" data-wab-lang="ru">RU</button> | |
| </div> | |
| <a class="wab-github" href="{_GITHUB_URL}" target="_blank" rel="noopener noreferrer" aria-label="{html.escape(t("github"))}">{_GITHUB_ICON}</a> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| def build_board(entries: list[LeaderboardEntry], view_id: str = "success") -> str: | |
| return f""" | |
| <div class="wab-board"> | |
| {build_table(entries, view_id)} | |
| </div> | |
| """ | |
| def build_shell(entries: list[LeaderboardEntry], view_id: str = "success") -> str: | |
| return build_shell_top(entries) + build_board(entries, view_id) | |
| def build_nav_js() -> str: | |
| """Client-side navigation for badge deep links (passed to demo.load js=).""" | |
| labels = ", ".join(f'"{key}": "{value}"' for key, value in _DETAIL_TAB_LABELS.items()) | |
| return f""" | |
| () => {{ | |
| if (window.__wabNavInit) return []; | |
| window.__wabNavInit = true; | |
| const TAB_LABELS = {{{labels}}}; | |
| function setPayload(value) {{ | |
| const root = document.getElementById("wab-nav-payload"); | |
| if (!root) return; | |
| const input = root.querySelector("textarea, input"); | |
| if (!input) return; | |
| const proto = input.tagName === "TEXTAREA" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; | |
| const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set; | |
| if (setter) setter.call(input, value); | |
| else input.value = value; | |
| input.dispatchEvent(new Event("input", {{ bubbles: true }})); | |
| }} | |
| window.wabOpenPage = function (label) {{ | |
| const root = document.getElementById("wab-page-tabs"); | |
| if (!root || !label) return; | |
| const wrap = root.querySelector(":scope > .tab-wrapper") || root; | |
| wrap.querySelectorAll("button").forEach((btn) => {{ | |
| if ((btn.textContent || "").trim() === label) btn.click(); | |
| }}); | |
| document.querySelectorAll("[data-wab-page]").forEach((el) => {{ | |
| el.classList.toggle("active", el.getAttribute("data-wab-page") === label); | |
| }}); | |
| }}; | |
| const CHROME = {{ | |
| en: {{ | |
| Details: "Details", | |
| Sections: "Sections", | |
| "UI taxonomy": "UI taxonomy", | |
| Metrics: "Metrics", | |
| Filters: "Filters", | |
| Run: "Run" | |
| }}, | |
| ru: {{ | |
| Details: "Подробности", | |
| Sections: "Разделы", | |
| "UI taxonomy": "Таксономия UI", | |
| Metrics: "Метрики", | |
| Filters: "Фильтры", | |
| Run: "Прогон" | |
| }} | |
| }}; | |
| function currentLang() {{ | |
| return window.__wabLang === "ru" ? "ru" : "en"; | |
| }} | |
| function setHidden(id, value) {{ | |
| const root = document.getElementById(id); | |
| if (!root) return false; | |
| const input = root.matches("textarea, input") ? root : root.querySelector("textarea, input"); | |
| if (!input) return false; | |
| const proto = input.tagName === "TEXTAREA" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; | |
| const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set; | |
| if (setter) setter.call(input, value); | |
| else input.value = value; | |
| input.dispatchEvent(new Event("input", {{ bubbles: true }})); | |
| input.dispatchEvent(new Event("change", {{ bubbles: true }})); | |
| return true; | |
| }} | |
| function clickHidden(id) {{ | |
| const root = document.getElementById(id); | |
| if (!root) return false; | |
| const btn = root.matches("button") ? root : root.querySelector("button"); | |
| if (!btn) return false; | |
| btn.click(); | |
| return true; | |
| }} | |
| let applyingChrome = false; | |
| function applyChrome(lang) {{ | |
| const map = CHROME[lang] || CHROME.en; | |
| if (applyingChrome) return; | |
| applyingChrome = true; | |
| try {{ | |
| const detail = document.getElementById("wab-detail-tabs"); | |
| if (detail) {{ | |
| detail.querySelectorAll('button[role="tab"]').forEach((btn) => {{ | |
| if (!btn.dataset.wabTabKey) btn.dataset.wabTabKey = (btn.textContent || "").trim(); | |
| const key = btn.dataset.wabTabKey; | |
| if (map[key] && btn.textContent !== map[key]) btn.textContent = map[key]; | |
| }}); | |
| }} | |
| const filters = document.getElementById("wab-filters"); | |
| if (filters) {{ | |
| const label = filters.querySelector(":scope > .label-wrap span, :scope > button span"); | |
| const node = label || filters.querySelector(":scope > .label-wrap, :scope > button"); | |
| if (node && map.Filters && (node.textContent.trim() === "Filters" || node.textContent.trim() === "Фильтры" || node.dataset.wabChrome === "filters")) {{ | |
| if (node.dataset.wabChrome !== "filters") node.dataset.wabChrome = "filters"; | |
| if (node.textContent.trim() !== map.Filters) node.textContent = map.Filters; | |
| }} | |
| }} | |
| const run = document.getElementById("wab-submission-dropdown"); | |
| if (run) {{ | |
| const lab = run.querySelector("label span, label"); | |
| if (lab && map.Run) {{ | |
| const current = (lab.textContent || "").trim(); | |
| if (current === "Run" || current === "Прогон" || lab.dataset.wabChrome === "run") {{ | |
| if (lab.dataset.wabChrome !== "run") lab.dataset.wabChrome = "run"; | |
| if (current !== map.Run) lab.textContent = map.Run; | |
| }} | |
| }} | |
| }} | |
| }} finally {{ | |
| applyingChrome = false; | |
| }} | |
| }} | |
| function syncPageMenu() {{ | |
| const root = document.getElementById("wab-page-tabs"); | |
| if (!root) return; | |
| const panels = [...root.querySelectorAll(":scope > .tabitem")]; | |
| const visible = panels.findIndex((el) => getComputedStyle(el).display !== "none"); | |
| const label = visible === 1 ? "About" : "Leaderboard"; | |
| document.querySelectorAll("[data-wab-page]").forEach((el) => {{ | |
| el.classList.toggle("active", el.getAttribute("data-wab-page") === label); | |
| }}); | |
| }} | |
| window.wabOpenDetailTab = function (tabId) {{ | |
| const label = TAB_LABELS[tabId]; | |
| if (!label) return; | |
| const root = document.getElementById("wab-detail-tabs") || document.querySelector(".wab-tabs"); | |
| if (!root) return; | |
| const buttons = root.querySelectorAll('button[role="tab"]'); | |
| for (const btn of buttons) {{ | |
| const key = btn.dataset.wabTabKey || (btn.textContent || "").trim(); | |
| if (key === label) {{ | |
| btn.click(); | |
| return; | |
| }} | |
| }} | |
| }}; | |
| function clickNavButton() {{ | |
| const root = document.getElementById("wab-nav-go"); | |
| if (!root) return false; | |
| const btn = root.matches("button") ? root : root.querySelector("button"); | |
| if (!btn) return false; | |
| btn.click(); | |
| return true; | |
| }} | |
| function scrollToDetail() {{ | |
| const anchor = document.getElementById("wab-detail-anchor"); | |
| if (anchor) anchor.scrollIntoView({{ behavior: "smooth", block: "start" }}); | |
| }} | |
| window.wabNavigate = function (payload) {{ | |
| if (!payload) return; | |
| window.__wabPendingNav = payload; | |
| setPayload(payload); | |
| const tab = payload.split(":", 1)[0] || "submission"; | |
| window.wabOpenDetailTab(tab); | |
| scrollToDetail(); | |
| setTimeout(clickNavButton, 60); | |
| }}; | |
| document.addEventListener("click", function (event) {{ | |
| const sortBtn = event.target.closest("[data-wab-sort]"); | |
| if (sortBtn) {{ | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const table = sortBtn.closest("table"); | |
| const tbody = table && table.querySelector("tbody"); | |
| if (!tbody) return; | |
| const key = sortBtn.getAttribute("data-wab-sort"); | |
| const attr = key === "tasks" ? "data-wab-tasks" : key === "strict" ? "data-wab-strict" : "data-wab-success"; | |
| const nextDir = sortBtn.classList.contains("is-active") && sortBtn.getAttribute("data-wab-dir") === "desc" ? "asc" : "desc"; | |
| table.querySelectorAll("[data-wab-sort]").forEach((el) => {{ | |
| el.classList.remove("is-active"); | |
| el.removeAttribute("data-wab-dir"); | |
| }}); | |
| sortBtn.classList.add("is-active"); | |
| sortBtn.setAttribute("data-wab-dir", nextDir); | |
| const rows = [...tbody.querySelectorAll("tr.wab-row")]; | |
| rows.sort((a, b) => {{ | |
| const rawA = a.getAttribute(attr); | |
| const rawB = b.getAttribute(attr); | |
| const numA = rawA === null || rawA === "" ? null : Number(rawA); | |
| const numB = rawB === null || rawB === "" ? null : Number(rawB); | |
| if (numA === null && numB === null) return 0; | |
| if (numA === null || Number.isNaN(numA)) return 1; | |
| if (numB === null || Number.isNaN(numB)) return -1; | |
| return nextDir === "asc" ? numA - numB : numB - numA; | |
| }}); | |
| rows.forEach((row, index) => {{ | |
| tbody.appendChild(row); | |
| const rank = row.querySelector(".wab-rank"); | |
| if (rank) rank.textContent = String(index + 1); | |
| }}); | |
| return; | |
| }} | |
| const traceTab = event.target.closest("[data-wab-trace-tab]"); | |
| if (traceTab && traceTab.closest(".wab-trace-detail-view")) {{ | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const root = traceTab.closest(".wab-trace-detail-view"); | |
| const tabId = traceTab.getAttribute("data-wab-trace-tab"); | |
| root.querySelectorAll("[data-wab-trace-tab]").forEach((el) => {{ | |
| el.classList.toggle("active", el === traceTab); | |
| }}); | |
| root.querySelectorAll("[data-wab-trace-panel]").forEach((panel) => {{ | |
| panel.classList.toggle("active", panel.getAttribute("data-wab-trace-panel") === tabId); | |
| }}); | |
| return; | |
| }} | |
| const stepBtn = event.target.closest("[data-wab-trace-step]"); | |
| if (stepBtn && stepBtn.closest(".wab-trace-detail-view")) {{ | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const root = stepBtn.closest(".wab-trace-detail-view"); | |
| const stepId = stepBtn.getAttribute("data-wab-trace-step"); | |
| root.querySelectorAll("[data-wab-trace-step]").forEach((el) => {{ | |
| el.classList.toggle("active", el === stepBtn); | |
| }}); | |
| root.querySelectorAll("[data-wab-trace-step-panel]").forEach((panel) => {{ | |
| panel.classList.toggle("active", panel.getAttribute("data-wab-trace-step-panel") === stepId); | |
| }}); | |
| return; | |
| }} | |
| const exportBtn = event.target.closest("[data-wab-trace-export]"); | |
| if (exportBtn && exportBtn.closest(".wab-trace-detail-view")) {{ | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const root = exportBtn.closest(".wab-trace-detail-view"); | |
| const dataEl = root.querySelector(".wab-trace-export-data"); | |
| if (!dataEl) return; | |
| const blob = new Blob([dataEl.textContent], {{ type: "application/json" }}); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = (root.querySelector(".wab-trace-crumb-current")?.textContent || "trace") + ".json"; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| return; | |
| }} | |
| const langBtn = event.target.closest("[data-wab-lang]"); | |
| if (langBtn) {{ | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const lang = langBtn.getAttribute("data-wab-lang") === "ru" ? "ru" : "en"; | |
| window.__wabLang = lang; | |
| try {{ localStorage.setItem("wab-lang", lang); }} catch (err) {{}} | |
| document.querySelectorAll("[data-wab-lang]").forEach((el) => {{ | |
| el.classList.toggle("active", el.getAttribute("data-wab-lang") === lang); | |
| }}); | |
| applyChrome(lang); | |
| setHidden("wab-lang-value", lang); | |
| setTimeout(() => clickHidden("wab-lang-go"), 30); | |
| return; | |
| }} | |
| const pageLink = event.target.closest("[data-wab-page]"); | |
| if (pageLink) {{ | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| window.wabOpenPage(pageLink.getAttribute("data-wab-page")); | |
| return; | |
| }} | |
| const jump = event.target.closest(".wab-jump-link"); | |
| if (jump) {{ | |
| event.preventDefault(); | |
| const href = jump.getAttribute("href") || ""; | |
| const target = href.startsWith("#") ? document.querySelector(href) : null; | |
| if (target) target.scrollIntoView({{ behavior: "smooth", block: "start" }}); | |
| else scrollToDetail(); | |
| return; | |
| }} | |
| const badge = event.target.closest("[data-wab-nav]"); | |
| if (!badge) return; | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| window.wabNavigate(badge.getAttribute("data-wab-nav")); | |
| }}); | |
| document.addEventListener("keydown", function (event) {{ | |
| if (event.key !== "Enter" && event.key !== " ") return; | |
| const badge = event.target.closest("[data-wab-nav]"); | |
| if (!badge) return; | |
| event.preventDefault(); | |
| window.wabNavigate(badge.getAttribute("data-wab-nav")); | |
| }}); | |
| function hidePageTablist() {{ | |
| const pageTabs = document.getElementById("wab-page-tabs"); | |
| const wrap = pageTabs && pageTabs.querySelector(":scope > .tab-wrapper"); | |
| if (wrap) wrap.style.display = "none"; | |
| }} | |
| hidePageTablist(); | |
| setTimeout(hidePageTablist, 300); | |
| let savedLang = "en"; | |
| try {{ savedLang = localStorage.getItem("wab-lang") === "ru" ? "ru" : "en"; }} catch (err) {{}} | |
| window.__wabLang = savedLang; | |
| applyChrome(savedLang); | |
| if (savedLang === "ru") {{ | |
| const kickLang = (attempt) => {{ | |
| const shell = document.querySelector(".wab-shell"); | |
| if (shell && shell.getAttribute("data-wab-current-lang") === "ru") return; | |
| setHidden("wab-lang-value", "ru"); | |
| const clicked = clickHidden("wab-lang-go"); | |
| if (attempt < 12 && (!clicked || !(shell && shell.getAttribute("data-wab-current-lang") === "ru"))) {{ | |
| setTimeout(() => kickLang(attempt + 1), 500); | |
| }} | |
| }}; | |
| setTimeout(() => kickLang(0), 400); | |
| }} | |
| let chromeQueued = false; | |
| const chromeObserver = new MutationObserver(() => {{ | |
| if (applyingChrome || chromeQueued) return; | |
| chromeQueued = true; | |
| requestAnimationFrame(() => {{ | |
| chromeQueued = false; | |
| if (applyingChrome) return; | |
| syncPageMenu(); | |
| applyChrome(currentLang()); | |
| }}); | |
| }}); | |
| chromeObserver.observe(document.body, {{ childList: true, subtree: true }}); | |
| document.addEventListener("input", function (event) {{ | |
| const input = event.target.closest(".wab-trace-step-filter"); | |
| if (!input) return; | |
| const root = input.closest(".wab-trace-detail-view"); | |
| if (!root) return; | |
| const query = (input.value || "").trim().toLowerCase(); | |
| root.querySelectorAll("[data-wab-trace-step]").forEach((btn) => {{ | |
| const label = btn.getAttribute("data-wab-step-label") || ""; | |
| btn.style.display = !query || label.includes(query) ? "" : "none"; | |
| }}); | |
| }}); | |
| return []; | |
| }} | |
| """ | |