WebPageBench / src /ui.py
ai-forever's picture
add fixes ui
cdb54bf
Raw History Blame Contribute Delete
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 [];
}}
"""