"""Agent trajectory list and detail views for the leaderboard.""" from __future__ import annotations import html import json import re from typing import Any from src.bench_config import load_bench_config from src.i18n import section_label_t, t from src.load_entries import entry_by_id, entry_label, load_raw_results from src.models import LeaderboardEntry from src.token_display import format_task_token_usage from src.ui import fmt_cost def parse_trace_nav(payload: str) -> tuple[str, str, str]: """Parse traces::[:] payloads.""" parts = (payload or "").split(":", 3) if len(parts) < 2 or parts[0] != "traces": return "list", "", "" view = parts[1] if len(parts) > 1 else "list" entry_id = parts[2] if len(parts) > 2 else "" test_name = parts[3] if len(parts) > 3 else "" return view, entry_id, test_name def _esc(value: object) -> str: return html.escape(str(value if value is not None else "—")) def _truncate(text: str, limit: int = 600) -> str: text = (text or "").strip() if len(text) <= limit: return text return text[: limit - 1].rstrip() + "…" def _task_section_id(test_name: str) -> str | None: config = load_bench_config() for section_id, meta in config.get("sections", {}).items(): if test_name in (meta.get("tasks") or []): return section_id return None def _task_section_label(test_name: str) -> str: section_id = _task_section_id(test_name) if section_id: return section_label_t(section_id) return "—" def format_duration(seconds: float | int | None) -> str: if seconds is None: return "—" try: total = float(seconds) except (TypeError, ValueError): return "—" if total < 60: return f"{total:.2f}s" minutes = int(total // 60) secs = int(total % 60) if minutes < 60: return f"{minutes}m {secs}s" hours = minutes // 60 minutes = minutes % 60 return f"{hours}h {minutes}m" def _extract_url_from_state(state: object) -> str | None: if not isinstance(state, str): return None match = re.search(r"url='([^']+)'", state) return match.group(1) if match else None def _format_named_actions(actions: list[Any]) -> str: parts: list[str] = [] for item in actions: if not isinstance(item, dict): continue for name, payload in item.items(): if isinstance(payload, dict): brief = ", ".join(f"{k}={v}" for k, v in list(payload.items())[:4]) parts.append(f"{name}({brief})") else: parts.append(f"{name}({payload})") return "; ".join(parts) def _format_gui_actions(actions: list[Any]) -> str: parts: list[str] = [] for item in actions: if not isinstance(item, dict): continue desc = item.get("description") or item.get("type") if desc: parts.append(str(desc)) return "; ".join(parts) def _format_browser_action(model_output: dict[str, Any] | None) -> str: if not model_output: return "" actions = model_output.get("action") if isinstance(actions, list) and actions: formatted = _format_named_actions(actions) if formatted: return formatted gui = model_output.get("actions") if isinstance(gui, list) and gui: formatted = _format_gui_actions(gui) if formatted: return formatted description = model_output.get("action_description") if isinstance(description, str) and description.strip(): return description.strip() return "" def _normalize_agent_step(step: dict[str, Any], harness: str) -> dict[str, str]: harness = (harness or "").lower() title = t("step_prefix") body_parts: list[str] = [] action = "" url = "" if "model_output" in step: model_output = step.get("model_output") or {} title = f"{t('step_prefix')} {step.get('step', step.get('metadata', {}).get('step_number', '?'))}" action = _format_browser_action(model_output) thinking = model_output.get("thinking") or model_output.get("next_goal") if thinking: body_parts.append(str(thinking)) results = step.get("result") if isinstance(results, list): for row in results[:2]: if isinstance(row, dict) and row.get("extracted_content"): body_parts.append(str(row["extracted_content"])) url = _extract_url_from_state(step.get("state")) or "" if not body_parts and step.get("state_message"): body_parts.append(_truncate(str(step["state_message"]), 400)) elif step.get("kind") == "message": title = t("user_message") if step.get("source") == "user" else t("agent_message") body_parts.append(str(step.get("content") or "")) elif step.get("kind") == "action": title = f"{t('action')} {step.get('index', '?')}" tool = step.get("tool") or step.get("action", {}).get("kind") action = str(tool or "action") payload = step.get("action") if isinstance(payload, dict): body_parts.append(_truncate(json.dumps(payload, ensure_ascii=False), 500)) else: title = f"{t('step_prefix')} {step.get('index', step.get('step', '?'))}" body_parts.append(_truncate(json.dumps(step, ensure_ascii=False), 500)) return { "title": title, "action": action, "body": _truncate("\n\n".join(part for part in body_parts if part), 1200), "url": url, } def _step_sidebar_label(step: dict[str, Any], harness: str) -> str: if "model_output" in step: formatted = _format_browser_action(step.get("model_output") or {}) if formatted: return formatted return f"{t('step_prefix')} {step.get('step', '?')}" if step.get("kind") == "message": return t("user_message") if step.get("source") == "user" else t("agent_message") if step.get("kind") == "action": return str(step.get("tool") or "action") return f"{t('step_prefix')} {step.get('index', step.get('step', '?'))}" def _step_kind_class(step: dict[str, Any]) -> str: if step.get("kind") == "user": return "user" if step.get("kind") == "message" and step.get("source") == "user": return "user" if step.get("kind") == "action" or "model_output" in step: return "tool" return "agent" def _ui_agent_steps(test: dict[str, Any], harness: str) -> list[dict[str, str]]: trajectory = test.get("trajectory") or {} agent_steps = (trajectory.get("agent") or {}).get("steps") or [] rows: list[dict[str, str]] = [ { "id": "0", "label": t("user_message"), "kind": "user", "title": t("task_title"), "action": "", "body": str(test.get("input") or ""), "url": str(test.get("task_url") or ""), } ] for raw_step in agent_steps: if not isinstance(raw_step, dict): continue if raw_step.get("kind") == "message" and raw_step.get("source") == "user": continue norm = _normalize_agent_step(raw_step, harness) rows.append( { "id": str(len(rows)), "label": _step_sidebar_label(raw_step, harness), "kind": _step_kind_class(raw_step), "title": norm["title"], "action": norm["action"], "body": norm["body"], "url": norm["url"], } ) return rows def _extract_action_rows(test: dict[str, Any], harness: str) -> list[dict[str, str]]: rows: list[dict[str, str]] = [] for step in _ui_agent_steps(test, harness)[1:]: if step["action"]: rows.append({"step": step["id"], "name": step["label"], "detail": step["action"]}) extended = test.get("extended_metrics") or {} if not rows and extended.get("action_count"): rows.append( { "step": "—", "name": "summary", "detail": f"action_count={extended.get('action_count')}, redundancy={extended.get('action_redundancy')}", } ) return rows def _coerce_positive_cost(value: object) -> float | None: if isinstance(value, bool) or not isinstance(value, (int, float)): return None amount = float(value) return amount if amount > 0 else None def _task_cost_usd(test: dict[str, Any], model: str) -> float | None: """Return per-task LLM cost from recorded usage or pricing table.""" usage_by_model = test.get("token_usage_by_model") if isinstance(usage_by_model, dict) and usage_by_model: total = 0.0 priced = False for usage in usage_by_model.values(): if not isinstance(usage, dict): continue cost = _coerce_positive_cost(usage.get("cost_usd")) if cost is not None: total += cost priced = True if priced: return total for container in ( test, test.get("token_usage") if isinstance(test.get("token_usage"), dict) else None, ): if not isinstance(container, dict): continue for key in ("cost_usd", "total_cost_usd", "cost", "total_cost"): cost = _coerce_positive_cost(container.get(key)) if cost is not None: return cost usage = test.get("token_usage") if not isinstance(usage, dict): return None try: from src.export_results import _ensure_repo_on_path _ensure_repo_on_path() from bench_eval.llm_cost import compute_token_cost_usd, load_pricing_table from bench_eval.token_usage import normalize_token_usage normalized = normalize_token_usage(usage) cost, _ = compute_token_cost_usd( model=model, prompt_tokens=normalized["prompt_tokens"], completion_tokens=normalized["completion_tokens"], total_tokens=normalized["total_tokens"], pricing_table=load_pricing_table(), ) return cost if cost is not None and cost > 0 else None except Exception: return None def _task_token_total(test: dict[str, Any]) -> float | None: usage = test.get("token_usage") if not isinstance(usage, dict): agent = test.get("agent") usage = agent.get("token_usage") if isinstance(agent, dict) else None if not isinstance(usage, dict): return None total = usage.get("total_tokens") if isinstance(total, (int, float)) and not isinstance(total, bool) and float(total) > 0: return float(total) prompt = usage.get("prompt_tokens") completion = usage.get("completion_tokens") prompt_n = float(prompt) if isinstance(prompt, (int, float)) and not isinstance(prompt, bool) else 0.0 completion_n = float(completion) if isinstance(completion, (int, float)) and not isinstance(completion, bool) else 0.0 summed = prompt_n + completion_n return summed if summed > 0 else None def _cost_share_from_run(test: dict[str, Any], entry: LeaderboardEntry) -> float | None: """Attribute the run total by this task's share of tokens. Most submissions store only the run total. Per-task token counts still add up to that total, so the task cost is total_cost * task_tokens / run_tokens. """ total_cost = entry.total_cost_usd total_tokens = entry.total_tokens if total_cost is None or total_cost <= 0 or not total_tokens: return None task_tokens = _task_token_total(test) if not task_tokens: return None return float(total_cost) * task_tokens / float(total_tokens) def _format_tokens(token_usage: dict[str, Any] | None) -> str: if not token_usage: return "—" prompt = token_usage.get("prompt_tokens") completion = token_usage.get("completion_tokens") if prompt is None and completion is None: total = token_usage.get("total_tokens") return str(total) if total is not None else "—" prompt_s = f"{float(prompt) / 1000:.2f}K" if isinstance(prompt, (int, float)) and prompt >= 1000 else str(prompt or 0) completion_s = ( f"{float(completion) / 1000:.2f}K" if isinstance(completion, (int, float)) and completion >= 1000 else str(completion or 0) ) return t("tokens_io", prompt=prompt_s, completion=completion_s) def _meta_cell(icon: str, label: str, value: object) -> str: return ( '
' f'
{icon}{_esc(label)}
' f'
{_esc(value)}
' "
" ) def _trace_slug(entry: LeaderboardEntry, test_name: str) -> str: short_model = entry.model.split("/")[-1] if "/" in entry.model else entry.model return f"{test_name}-{entry.harness}-{short_model}" def _detail_tab_button(tab_id: str, label: str, count: int | None = None, *, active: bool = False) -> str: count_html = f'{count}' if count is not None else "" active_cls = " active" if active else "" return ( f'' ) def trace_summaries_for_entry(entry: LeaderboardEntry) -> list[dict[str, Any]]: raw = load_raw_results(entry) if not raw: return [] rows: list[dict[str, Any]] = [] for test in raw.get("tests") or []: if not isinstance(test, dict): continue test_name = str(test.get("test_name") or "") if not test_name: continue trajectory = test.get("trajectory") or {} agent_steps = (trajectory.get("agent") or {}).get("steps") or [] dab_events = trajectory.get("dab_events") or [] ui = test.get("ui_taxonomy") or {} rows.append( { "test_name": test_name, "section": _task_section_label(test_name), "success": bool(test.get("success")), "duration": test.get("duration_seconds"), "agent_steps": test.get("agent_steps") or len(agent_steps), "dab_events": len(dab_events), "input": str(test.get("input") or ""), "domain": ui.get("domain") or "", "primary_class": (ui.get("checked_classes") or [None])[0], } ) rows.sort(key=lambda row: row["test_name"]) return rows def _status_badge(success: bool) -> str: css = "wab-trace-status pass" if success else "wab-trace-status fail" label = t("pass") if success else t("fail") return f'{label}' def _trace_pill(entry_id: str, inner: str, *, active: bool = False) -> str: active_cls = " active" if active else "" return ( f'' ) def _trace_card(entry_id: str, test_name: str, inner: str, *, visible: bool = True) -> str: hidden = "" if visible else ' style="display:none"' payload = f"traces:detail:{entry_id}:{test_name}" return ( f'" ) def _trace_link(payload: str, inner: str, css_class: str = "wab-trace-link") -> str: return ( f'" ) def build_traces_shell( entries: list[LeaderboardEntry], *, view: str = "list", entry_id: str = "", test_name: str = "", ) -> str: if not entries: return ( '
' '

Agent Traces

' '

No submissions yet. Export results after eval.

' "
" ) if view == "detail" and entry_id and test_name: return build_trace_detail(entries, entry_id, test_name) selected = entry_by_id(entries, entry_id) or entries[0] selected_id = selected.entry_id return _build_traces_list(entries, selected_id) def _build_traces_list(entries: list[LeaderboardEntry], selected_id: str) -> str: selected_entry = entry_by_id(entries, selected_id) or entries[0] selected_summaries = trace_summaries_for_entry(selected_entry) pills: list[str] = [] for entry in entries: count = ( len(selected_summaries) if entry.entry_id == selected_entry.entry_id else int(entry.total_tasks or 0) ) active = entry.entry_id == selected_id label = html.escape(entry.model.split("/")[-1] if "/" in entry.model else entry.model) harness = html.escape(entry.harness) inner = ( f'{label}' f'{harness}' f'{count}' ) pills.append(_trace_pill(entry.entry_id, inner, active=active)) cards: list[str] = [] for row in selected_summaries: test_name = row["test_name"] short_name = test_name.replace("bench_", "", 1) if test_name.startswith("bench_") else test_name tags = [] if row["domain"]: tags.append(f'{_esc(row["domain"])}') if row["primary_class"]: tags.append(f'{_esc(row["primary_class"])}') tags_html = "".join(tags) card_inner = ( f'
' f'
' f'
' f'#{_esc(short_name)}' f'{_esc(row["section"])}' f"{_status_badge(row['success'])}" f'⏱ {_esc(format_duration(row["duration"]))}' f'{row["agent_steps"]} steps · {row["dab_events"]} checks' f"
" f'

{_esc(_truncate(row["input"], 220))}

' f'
{tags_html}
' f"
" ) cards.append(_trace_card(selected_entry.entry_id, test_name, card_inner)) body = "".join(cards) if cards else '

No task traces in this run.

' return ( '
' '

Agent Traces

' '

Step-by-step agent actions and condition checks from every eval run.

' f'
{"".join(pills)}
' f'
{body}
' "
" ) def _find_test(entry: LeaderboardEntry, test_name: str) -> dict[str, Any] | None: raw = load_raw_results(entry) if not raw: return None for test in raw.get("tests") or []: if isinstance(test, dict) and test.get("test_name") == test_name: return test return None def build_trace_detail( entries: list[LeaderboardEntry], entry_id: str, test_name: str, *, embedded: bool = False, ) -> str: entry = entry_by_id(entries, entry_id) if entry is None: return f'

{_esc(t("run_not_found"))}

' test = _find_test(entry, test_name) if test is None: return f'

{_esc(t("trace_not_found"))}

' raw = load_raw_results(entry) or {} run = raw.get("run") or {} trajectory = test.get("trajectory") or {} dab_events = trajectory.get("dab_events") or [] harness = entry.harness or (test.get("agent_harness") or "") ui = test.get("ui_taxonomy") or {} dab_check = test.get("dab_check") or {} agent = test.get("agent") or {} ui_steps = _ui_agent_steps(test, harness) action_rows = _extract_action_rows(test, harness) short_name = test_name.replace("bench_", "", 1) if test_name.startswith("bench_") else test_name slug = _trace_slug(entry, test_name) provider = entry.provider or run.get("provider") or "—" domain = ui.get("domain") or "—" section = _task_section_label(test_name) token_usage = test.get("token_usage") or agent.get("token_usage") or {} task_cost = _task_cost_usd(test, entry.model) if task_cost is None: task_cost = _cost_share_from_run(test, entry) model_usage = format_task_token_usage( test, primary_model=entry.model, ouroboros_model_slots=entry.ouroboros_model_slots, ) success = bool(test.get("success")) meta_grid = "".join( [ _meta_cell("#", t("task"), f"#{short_name}"), _meta_cell("🧪", t("test_case"), test_name), _meta_cell("🤖", t("model"), entry.model), _meta_cell("☁", t("provider"), provider), _meta_cell("🏷", t("category"), f"{section} / {domain}"), _meta_cell("🌐", t("harness"), harness), _meta_cell("🖼", t("input"), entry.input_modality), _meta_cell("👣", t("steps"), test.get("agent_steps") or max(0, len(ui_steps) - 1)), _meta_cell("🪙", t("tokens"), _format_tokens(token_usage)), _meta_cell("🧩", t("model_usage"), model_usage), _meta_cell("💰", t("cost"), fmt_cost(task_cost)), _meta_cell("⏱", t("duration"), format_duration(test.get("duration_seconds"))), _meta_cell("🚩", t("result"), t("pass") if success else t("fail")), ] ) step_nav_items: list[str] = [] step_panels: list[str] = [] for step in ui_steps: active = " active" if step["id"] == "0" else "" step_nav_items.append( f'" ) url_line = f'
{_esc(step["url"])}
' if step["url"] else "" action_line = ( f'
{_esc(t("action"))}: {_esc(step["action"])}
' if step["action"] else "" ) step_panels.append( f'
' f'
' f"{_esc(step['title'])}
" f"{url_line}{action_line}" f'
{_esc(step["body"])}
' "
" ) action_cards = [] for row in action_rows: action_cards.append( '
' f'
{_esc(t("step_prefix"))} {row["step"]}: {_esc(row["name"])}
' f'
{_esc(row["detail"])}
' "
" ) dab_cards = [] for event in dab_events[:120]: if not isinstance(event, dict): continue event_name = event.get("event_name") or event.get("event_data", {}).get("type") or "event" event_data = event.get("event_data") or {} summary = _truncate(json.dumps(event_data, ensure_ascii=False), 800) dab_cards.append( '
' f'
{_esc(event_name)}' f'{_esc(event.get("event_ts"))}
' f'
{_esc(summary)}
' "
" ) checks = dab_check.get("check_results") or [] check_rows = [] for row in checks: if not isinstance(row, dict): continue ok = _condition_ok(row) mark = "✓" if ok else "✗" css = "wab-trace-check-pass" if ok else "wab-trace-check-fail" row_cls = "pass" if ok else "fail" extra = f" [group:{row.get('group')}]" if row.get("group") else "" check_rows.append( f'
  • {mark} ' f"{_esc(_condition_label(row))}{_esc(extra)}
  • " ) checks_html = f'
      {"".join(check_rows)}
    ' if check_rows else "

    —

    " export_json = json.dumps(test, ensure_ascii=False).replace("{_esc(t("no_actions"))}

    ' requests_body = "".join(dab_cards) if dab_cards else f'

    {_esc(t("no_checks"))}

    ' if embedded: crumb = f'' else: back = _trace_link(f"traces:list:{entry_id}", "← Traces", "wab-trace-back") crumb = ( f'' ) return ( f'
    ' f"{crumb}" '
    ' f'

    {_esc(test.get("input") or "")}

    ' f'
    {meta_grid}
    ' '
    ' f"{_status_badge(success)}" f'' "
    " f'' "
    " f'
    {tabs}
    ' '
    ' '
    ' '
    ' '" f'
    {"".join(step_panels)}
    ' "
    " "
    " '
    ' f'
    {actions_body}
    ' "
    " '
    ' f'
    {requests_body}
    ' "
    " '
    ' f"

    all_passed: {_esc(dab_check.get('all_passed'))}

    " f"{checks_html}" f"

    failed_conditions: {_esc(', '.join(dab_check.get('failed_conditions') or []) or '—')}

    " "
    " '
    ' f"

    {_esc(t('agent_output'))}

    " f'
    {_esc(_truncate(str(test.get("actual_output") or ""), 6000))}
    ' f"

    {_esc(t('final_result'))}

    " f'
    {_esc(_truncate(str(agent.get("final_result") or ""), 4000))}
    ' f"

    {_esc(t('agent_error'))}

    " f'
    {_esc(test.get("agent_error") or "—")}
    ' "
    " "
    " "
    " ) def _condition_ok(row: dict) -> bool: if row.get("group"): value = row.get("group_success") if value is None: value = row.get("passed") return bool(value) if "success" in row: return bool(row.get("success")) return bool(row.get("passed")) def _condition_label(row: dict) -> str: return str(row.get("description") or row.get("event_name") or row.get("condition") or row.get("name") or "condition") def _meta_plain(label: str, value: object, *, html_value: str | None = None) -> str: shown = html_value if html_value is not None else _esc(value) return ( '
    ' f'
    {_esc(label)}
    ' f'
    {shown}
    ' "
    " ) def _run_header(entry: LeaderboardEntry) -> str: success = f"{entry.success_pct}% ({entry.passed_tasks}/{entry.total_tasks})" return ( '
    ' f'

    {_esc(entry.model)} · {_esc(entry.harness)}

    ' '
    ' f"{_meta_plain(t('success'), success)}" f"{_meta_plain(t('input'), entry.input_modality)}" f"{_meta_plain(t('duration'), format_duration(entry.total_duration_seconds))}" f"{_meta_plain(t('cost'), fmt_cost(entry.total_cost_usd))}" "
    " "
    " ) def _run_task_detail(entry: LeaderboardEntry, test_name: str) -> str: test = _find_test(entry, test_name) if test is None: return f'

    {_esc(t("task_not_found"))}

    ' trajectory = test.get("trajectory") or {} events = [event for event in (trajectory.get("dab_events") or []) if isinstance(event, dict)] names = [str(name) for name in (trajectory.get("dab_event_names") or [])] if not names: names = [str(event.get("event_name") or "event") for event in events] check = test.get("dab_check") or {} results = [row for row in (check.get("check_results") or []) if isinstance(row, dict)] if results: items = [] for row in results: ok = _condition_ok(row) mark = "✓" if ok else "✗" css = "wab-trace-check-pass" if ok else "wab-trace-check-fail" extra = f" [group:{row.get('group')}]" if row.get("group") else "" row_cls = "pass" if ok else "fail" items.append( f'
  • {mark} {_esc(_condition_label(row))}{_esc(extra)}
  • ' ) checks_html = f'
      {"".join(items)}
    ' else: checks_html = "

    —

    " shown = events[:80] cards = [] for event in shown: name = event.get("event_name") or "event" payload = _truncate(json.dumps(event.get("event_data") or {}, ensure_ascii=False, indent=2), 2000) cards.append( '
    ' '
    ' f"{_esc(name)}" f"{_esc(event.get('event_ts') or '')}" "
    " f'
    {_esc(payload)}
    ' "
    " ) extra_note = "" if len(events) > len(shown): extra_note = f'

    {_esc(t("showing_events", shown=len(shown), total=len(events)))}

    ' event_html = "".join(cards) if cards else f'

    {_esc(t("no_events"))}

    ' prompt = test.get("task") or test.get("input") or "" if check.get("all_passed") is True: result = "PASS" elif check: result = "FAIL" else: result = "PASS" if test.get("success") else "FAIL" passed = check.get("passed") total = check.get("total") result_badge = _status_badge(result == "PASS") result_html = result_badge if passed is None else f"{result_badge} {_esc(f'{passed}/{total}')}" return ( '
    ' f'

    {_esc(prompt)}

    ' '
    ' f"{_meta_plain(t('test_case'), test_name)}" f"{_meta_plain(t('result'), '', html_value=result_html)}" f"{_meta_plain(t('events'), len(events))}" f"{_meta_plain(t('duration'), format_duration(test.get('duration_seconds')))}" "
    " "
    " f"

    {_esc(t('conditions'))}

    " f"{checks_html}" f"

    {_esc(t('event_chain'))}

    " f'

    {_esc(" → ".join(names) or "—")}

    ' f'
    {event_html}
    ' f"{extra_note}" ) def build_run_page(entries: list[LeaderboardEntry], entry_id: str, test_name: str = "") -> str: """Run page: task list plus the selected task's prompt, conditions, and events.""" entry = entry_by_id(entries, entry_id) if entry is None: return '

    Run not found.

    ' summaries = trace_summaries_for_entry(entry) header = _run_header(entry) if not summaries: return ( '
    ' f"{header}" f'

    {t("task_list_unavailable")}

    ' "
    " ) names = [row["test_name"] for row in summaries] selected = test_name if test_name in names else names[0] items = [] for row in summaries: active = " active" if row["test_name"] == selected else "" tone = " pass" if row["success"] else " fail" short = row["test_name"].replace("bench_", "", 1) if str(row["test_name"]).startswith("bench_") else row["test_name"] payload = f"submission:{entry.entry_id}:{row['test_name']}" items.append( f'" ) return ( '
    ' f"{header}" '
    ' f'' f'
    {build_trace_detail(entries, entry.entry_id, selected, embedded=True)}
    ' "
    " "
    " ) def build_trace_js_on_load() -> str: """Gradio 6 js_on_load for trace list/detail interactions (must include 'navigate').""" return """ function filterTraceEntry(root, entryId) { if (!root || !entryId) return; root.querySelectorAll(".wab-trace-pill").forEach((pill) => { pill.classList.toggle("active", pill.getAttribute("data-wab-trace-filter") === entryId); }); root.querySelectorAll(".wab-trace-card").forEach((card) => { card.style.display = card.getAttribute("data-wab-entry-id") === entryId ? "flex" : "none"; }); } element.addEventListener("click", (event) => { const tab = event.target.closest("[data-wab-trace-tab]"); if (tab && tab.closest(".wab-trace-detail-view")) { event.preventDefault(); const root = tab.closest(".wab-trace-detail-view"); const tabId = tab.getAttribute("data-wab-trace-tab"); root.querySelectorAll("[data-wab-trace-tab]").forEach((el) => { el.classList.toggle("active", el === tab); }); 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(); 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(); 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 pill = event.target.closest("[data-wab-trace-filter]"); if (pill) { event.preventDefault(); const entryId = pill.getAttribute("data-wab-trace-filter"); if (!entryId) return; trigger("navigate", { payload: `traces:list:${entryId}` }); return; } const navBtn = event.target.closest("[data-wab-trace]"); if (!navBtn) return; event.preventDefault(); const payload = navBtn.getAttribute("data-wab-trace"); if (!payload) return; if (payload.startsWith("traces:list:")) { trigger("navigate", { payload }); return; } if (window.wabOpenDetailTab) window.wabOpenDetailTab("traces"); const anchor = document.getElementById("wab-detail-anchor"); if (anchor) anchor.scrollIntoView({ behavior: "smooth", block: "start" }); trigger("navigate", { payload }); }); element.addEventListener("input", (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"; }); }); """