diff --git a/workshops/agent_arena/README.md b/workshops/agent_arena/README.md
index d255032..6412aea 100644
--- a/workshops/agent_arena/README.md
+++ b/workshops/agent_arena/README.md
@@ -24,7 +24,7 @@ source of truth for benchmark results.
## What you get
-A single web app (`web/`, http://localhost:5174) with four tabs:
+A focused web app (`web/`, http://localhost:5174) with two tabs:
- **Leaderboard** — the contest results: every model×prompt config ranked by
accuracy, **cost-per-correct-answer** (the headline), latency, per-tier
@@ -34,14 +34,9 @@ A single web app (`web/`, http://localhost:5174) with four tabs:
generated SQL → error → tokens → span timings). A **"View conversation"**
button replays the agent's session **live from the LangFuse API**, and an
**LLM-judge** column scores SQL quality.
-- **Countdown** — a live-event "stage" screen for demos: a presenter countdown
- timer, the model families as contenders, and the current best accuracy per
- family (read live from the latest run).
- **Chat** — the production chatbot: ask a question against a picked
model+prompt config and watch the SQL, cost, and latency; rate each answer
👍/👎, which is written back to the trace as a **LangFuse score**.
-- **Architecture** — an animated React Flow diagram of the whole system.
-
Plus the serving API (`serving/api.py`): **`POST /ask`** (run the agent live and
return SQL/results/cost/latency, traced to LangFuse) and **`POST /feedback`**
(attach a 👍/👎 score to a trace) — the same endpoints the Chat tab calls.
@@ -59,9 +54,6 @@ Golden dataset → benchmark harness → agent → OpenRouter
leaderboard API/UI
```
-The web app's **Architecture** tab renders the live component graph from
-`web/src/diagram/graph.js`.
-
**The key idea — one service per job:**
- **ClickHouse** is the application database: it holds the business data and
executes generated and golden SQL.
@@ -185,7 +177,6 @@ Everything tunable lives in [`config.yaml`](config.yaml):
- **`models`** — OpenRouter model id, display name, family, and per-1M-token input/output prices (used for cost).
- **`prompts`** — the strategies `P1_zeroshot` … `P3_dialect`.
- **`grid`** — which models × prompts to actually run (`["*"]` = all).
-- **`profiles`** — curated presets (Budget tier / Frontier / Everything) for the web UI's "Run benchmark" panel.
- **`clickhouse.query_limits`** — the server-side caps enforced on agent SQL.
Adding a model or prompt is a config edit, not a code change.
diff --git a/workshops/agent_arena/arena/config.py b/workshops/agent_arena/arena/config.py
index 0037f14..10fc1cc 100644
--- a/workshops/agent_arena/arena/config.py
+++ b/workshops/agent_arena/arena/config.py
@@ -81,12 +81,6 @@ class GridCfg(BaseModel):
prompts: list[str]
-class ProfileCfg(BaseModel):
- name: str
- desc: str = ""
- models: list[str] = []
-
-
class Config(BaseModel):
clickhouse: ClickHouseCfg
openrouter: OpenRouterCfg
@@ -95,7 +89,6 @@ class Config(BaseModel):
models: list[ModelCfg]
prompts: list[PromptCfg]
grid: GridCfg
- profiles: list[ProfileCfg] = []
def resolved_grid(self) -> tuple[list[str], list[str]]:
all_models = [m.name for m in self.models]
diff --git a/workshops/agent_arena/config.yaml b/workshops/agent_arena/config.yaml
index 7a63ac3..fd04976 100644
--- a/workshops/agent_arena/config.yaml
+++ b/workshops/agent_arena/config.yaml
@@ -92,16 +92,3 @@ prompts:
grid:
models: ["*"]
prompts: ["*"]
-
-# Preset model selections for the web UI's "Run benchmark" panel. Each profile is
-# a one-click curated comparison; models reference the names above.
-profiles:
- - name: "Open weight"
- desc: "Open-weight contenders — DeepSeek, Qwen, GLM."
- models: [deepseek-v4-flash, qwen3.7-flash, glm-4.7-flash]
- - name: "Proprietary"
- desc: "Closed-weight models — Claude, GPT, Gemini."
- models: [claude-sonnet-5, gpt-5.6-luna, gemini-flash-lite]
- - name: "Everything"
- desc: "All six models — the open-vs-proprietary contest."
- models: [claude-sonnet-5, gpt-5.6-luna, gemini-flash-lite, deepseek-v4-flash, qwen3.7-flash, glm-4.7-flash]
diff --git a/workshops/agent_arena/dashboard/app.py b/workshops/agent_arena/dashboard/app.py
index 1902639..699df90 100644
--- a/workshops/agent_arena/dashboard/app.py
+++ b/workshops/agent_arena/dashboard/app.py
@@ -151,106 +151,9 @@ def meta():
"datasets_url": f"{_lf_base}/datasets" if _lf_base else None}
-# --- Run the benchmark from the UI (spawns the harness as a subprocess) -------
-# Local/demo use only: this lets the web app trigger `python -m eval.harness`.
-# The API process must have an OpenRouter API key in its environment.
-import subprocess
-import sys
-import threading
-from pathlib import Path
-
-_ROOT = Path(__file__).resolve().parent.parent
-_run_state = {"running": False, "run_id": None, "started_at": None,
- "lines": [], "returncode": None}
-_run_lock = threading.Lock()
-
-
@app.get("/api/grid-options")
def grid_options():
- """Prompts available to run (from config.yaml), with descriptions for hovers.
- Models now come from the catalog endpoint /api/models."""
+ """Prompt descriptions used by leaderboard labels and tooltips."""
return {
"prompts": [{"name": p.name, "desc": p.desc or p.name} for p in _cfg.prompts],
}
-
-
-@app.get("/api/profiles")
-def profiles():
- """Preset model selections (config.yaml `profiles`) resolved to invocable ids."""
- by_name = {m.name: m.id for m in _cfg.models}
- return [{"name": p.name, "desc": p.desc,
- "model_ids": [by_name[n] for n in p.models if n in by_name]}
- for p in _cfg.profiles]
-
-
-# --- Model catalog ------------------------------------------------------------
-@app.get("/api/models")
-def models():
- """Curated config.yaml models grouped by family, with LIVE OpenRouter prices
- (falls back to config.yaml prices with a `degraded` reason if unreachable)."""
- prices, degraded = {}, None
- try:
- from agents.llm import fetch_openrouter_prices
- prices = fetch_openrouter_prices(_cfg.openrouter.base_url)
- except Exception as e: # noqa: BLE001
- degraded = str(e)
- fam_map = {}
- for m in _cfg.models:
- pin, pout = prices.get(m.id, (m.price_per_1m_in, m.price_per_1m_out))
- fam_map.setdefault(m.family, []).append(
- {"id": m.id, "name": m.name, "price_per_1m_in": pin,
- "price_per_1m_out": pout, "in_default": True})
- families = [{"family": fam, "models": ms} for fam, ms in fam_map.items()]
- return {"provider": "openrouter", "region": "OpenRouter",
- "families": families, "degraded": degraded}
-
-
-def _stream_harness(cmd: list[str]):
- proc = subprocess.Popen(cmd, cwd=str(_ROOT), env=os.environ.copy(),
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
- text=True, bufsize=1)
- for line in proc.stdout:
- line = line.rstrip()
- with _run_lock:
- _run_state["lines"].append(line)
- _run_state["lines"] = _run_state["lines"][-400:] # keep tail
- proc.wait()
- with _run_lock:
- _run_state["running"] = False
- _run_state["returncode"] = proc.returncode
- _results_cache.update(loaded_at=0.0, model=None)
-
-
-@app.post("/api/run")
-def start_run(body: dict):
- with _run_lock:
- if _run_state["running"]:
- return {"ok": False, "error": "a run is already in progress",
- "run_id": _run_state["run_id"]}
- run_id = body.get("run_id") or f"ui-{int(time.time())}"
- cmd = [sys.executable, "-m", "eval.harness", "--run-id", run_id]
- models = body.get("models") or []
- if models and isinstance(models[0], dict):
- # full specs from the live-catalog browser → temp models-file
- import json
- specs = [{"id": m["id"], "name": m["name"], "family": m.get("family", "other"),
- "price_per_1m_in": float(m.get("price_in") or 0),
- "price_per_1m_out": float(m.get("price_out") or 0)} for m in models]
- (_ROOT / ".run").mkdir(exist_ok=True)
- mf = _ROOT / ".run" / f"models-{run_id}.json"
- mf.write_text(json.dumps(specs))
- cmd += ["--models-file", str(mf)]
- elif models:
- cmd += ["--models", ",".join(models)]
- if body.get("prompts"):
- cmd += ["--prompts", ",".join(body["prompts"])]
- _run_state.update(running=True, run_id=run_id, started_at=time.time(),
- lines=[f"$ {' '.join(cmd[2:])}"], returncode=None)
- threading.Thread(target=_stream_harness, args=(cmd,), daemon=True).start()
- return {"ok": True, "run_id": run_id}
-
-
-@app.get("/api/run/status")
-def run_status():
- with _run_lock:
- return dict(_run_state)
diff --git a/workshops/agent_arena/tests/test_dashboard_langfuse_results.py b/workshops/agent_arena/tests/test_dashboard_langfuse_results.py
index 5175b13..980e84c 100644
--- a/workshops/agent_arena/tests/test_dashboard_langfuse_results.py
+++ b/workshops/agent_arena/tests/test_dashboard_langfuse_results.py
@@ -209,6 +209,16 @@ def test_health_endpoint_does_not_load_langfuse(monkeypatch):
assert dashboard.healthz() == {"status": "ok"}
+def test_dashboard_api_is_read_only_for_workshop_learners():
+ """The public dashboard must not expose its former demo run-builder API."""
+ paths = {route.path for route in dashboard.app.routes}
+ assert "/api/grid-options" in paths
+ assert "/api/models" not in paths
+ assert "/api/profiles" not in paths
+ assert "/api/run" not in paths
+ assert "/api/run/status" not in paths
+
+
def test_reused_run_id_keeps_only_the_newest_question_result():
"""A repeated run/config/question must not inflate counts or preserve stale scores."""
base = {"run_id": "r1", "config_id": "m__p", "model_name": "m",
diff --git a/workshops/agent_arena/web/README.md b/workshops/agent_arena/web/README.md
index ffa828a..4a79d63 100644
--- a/workshops/agent_arena/web/README.md
+++ b/workshops/agent_arena/web/README.md
@@ -1,16 +1,6 @@
# AgentArena — Web UI
-A single [React](https://react.dev) ([Vite](https://vite.dev)) SPA with four tabs:
-
-- **Countdown** — a live-event "stage" screen for demos: a presenter countdown timer,
- the model families as contenders, and the current best accuracy per family (read live
- from the latest run).
-- **Architecture** — an animated [React Flow](https://reactflow.dev) (`@xyflow/react`)
- data-flow diagram of the whole system. Packets travel along each edge to show
- flow direction; edge colors group flow types, node colors show where each
- component runs (legend top-right). Edge labels are short so they don't cover the
- arrows — **hover an edge label** to see the full description. Nodes are
- draggable; drag the canvas to pan, scroll to zoom.
+A focused [React](https://react.dev) ([Vite](https://vite.dev)) SPA with two tabs:
- **Leaderboard** — the contest results, read from LangFuse Experiments through its
Public API:
run selector, winner cards, sortable leaderboard ranked by
@@ -28,19 +18,15 @@ A single [React](https://react.dev) ([Vite](https://vite.dev)) SPA with four tab
config, see the generated SQL/results/cost/latency, and rate each answer
👍/👎 — feedback is written back to the trace as a LangFuse score.
-Diagram layout is computed by **dagre** (layered left→right with crossing
-minimization); edges use orthogonal smoothstep routing.
-
## Run
```bash
cd web
npm install
-npm run dev # → http://localhost:5174 (all four tabs)
+npm run dev # → http://localhost:5174
```
-The **Leaderboard** and **Chat** tabs need the dashboard/serving APIs running
-(**Countdown** and **Architecture** need nothing):
+The **Leaderboard** and **Chat** tabs need the dashboard/serving APIs running:
```bash
# from the repo root, in other shells:
@@ -54,15 +40,10 @@ The UI calls `http://localhost:8000` (dashboard) and `http://localhost:8100`
CORS enabled for the SPA.
## Files
-- `src/App.jsx` — tab shell (Countdown / Architecture / Leaderboard / Chat).
+- `src/App.jsx` — tab shell (Leaderboard / Chat).
- `src/ui.jsx` — shared presentational atoms (brand lock, icons, ClickHouse logomark).
- `src/api.js` — dashboard/serving API bases + fetch helpers (`VITE_API_BASE`, `VITE_SERVING_BASE`).
-- `src/diagram/graph.js` — architecture model (nodes + edges, env colors). Edit here to change the diagram.
-- `src/diagram/layout.js` — dagre layered LR auto-layout.
-- `src/diagram/FlowDiagram.jsx` — React Flow canvas + legends + minimap.
-- `src/diagram/nodes/CardNode.jsx`, `src/diagram/edges/AnimatedFlowEdge.jsx` — renderers.
- `src/leaderboard/Leaderboard.jsx` — leaderboard UI (fetches the dashboard API).
-- `src/countdown/Countdown.jsx` — the live-event stage screen.
- `src/chat/Chat.jsx` — the production chat UI (calls the serving API's `/ask` + `/feedback`).
> The leaderboard data comes from `dashboard/app.py` (a FastAPI adapter over the
diff --git a/workshops/agent_arena/web/package-lock.json b/workshops/agent_arena/web/package-lock.json
index ef2aece..9cbf687 100644
--- a/workshops/agent_arena/web/package-lock.json
+++ b/workshops/agent_arena/web/package-lock.json
@@ -1,24 +1,46 @@
{
- "name": "chatbi-arena-diagram",
+ "name": "agent-arena-dashboard",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "chatbi-arena-diagram",
+ "name": "agent-arena-dashboard",
"version": "1.0.0",
"dependencies": {
- "@dagrejs/dagre": "^1.1.8",
- "@xyflow/react": "^12.3.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-tooltip": "^5.30.1"
},
"devDependencies": {
+ "@testing-library/react": "^16.3.2",
"@vitejs/plugin-react": "^4.3.4",
- "vite": "^6.0.7"
+ "jsdom": "^26.1.0",
+ "vite": "^6.0.7",
+ "vitest": "^3.2.7"
}
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -253,6 +275,16 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
@@ -301,22 +333,119 @@
"node": ">=6.9.0"
}
},
- "node_modules/@dagrejs/dagre": {
- "version": "1.1.8",
- "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.8.tgz",
- "integrity": "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==",
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "@dagrejs/graphlib": "2.2.4"
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
}
},
- "node_modules/@dagrejs/graphlib": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
- "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
"license": "MIT",
"engines": {
- "node": ">17.0.0"
+ "node": ">=18"
}
},
"node_modules/@esbuild/aix-ppc64": {
@@ -1193,6 +1322,63 @@
"win32"
]
},
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1238,55 +1424,24 @@
"@babel/types": "^7.28.2"
}
},
- "node_modules/@types/d3-color": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
- "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
- "license": "MIT"
- },
- "node_modules/@types/d3-drag": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
- "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-selection": "*"
- }
- },
- "node_modules/@types/d3-interpolate": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
- "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@types/d3-color": "*"
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
}
},
- "node_modules/@types/d3-selection": {
- "version": "3.0.11",
- "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
- "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/@types/d3-transition": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
- "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-selection": "*"
- }
- },
- "node_modules/@types/d3-zoom": {
- "version": "3.0.8",
- "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
- "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
- "license": "MIT",
- "dependencies": {
- "@types/d3-interpolate": "*",
- "@types/d3-selection": "*"
- }
- },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -1315,46 +1470,175 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
- "node_modules/@xyflow/react": {
- "version": "12.11.0",
- "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz",
- "integrity": "sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==",
+ "node_modules/@vitest/expect": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
+ "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
+ "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@xyflow/system": "0.0.77",
- "classcat": "^5.0.3",
- "zustand": "^4.4.0"
+ "@vitest/spy": "3.2.7",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@types/react": ">=17",
- "@types/react-dom": ">=17",
- "react": ">=17",
- "react-dom": ">=17"
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
- "@types/react": {
+ "msw": {
"optional": true
},
- "@types/react-dom": {
+ "vite": {
"optional": true
}
}
},
- "node_modules/@xyflow/system": {
- "version": "0.0.77",
- "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.77.tgz",
- "integrity": "sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==",
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
+ "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
+ "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.7",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
+ "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
+ "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
+ "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
"license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
"dependencies": {
- "@types/d3-drag": "^3.0.7",
- "@types/d3-interpolate": "^3.0.4",
- "@types/d3-selection": "^3.0.10",
- "@types/d3-transition": "^3.0.8",
- "@types/d3-zoom": "^3.0.8",
- "d3-drag": "^3.0.0",
- "d3-interpolate": "^3.0.1",
- "d3-selection": "^3.0.0",
- "d3-zoom": "^3.0.0"
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
}
},
"node_modules/baseline-browser-mapping": {
@@ -1404,6 +1688,16 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001797",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
@@ -1425,11 +1719,32 @@
],
"license": "CC-BY-4.0"
},
- "node_modules/classcat": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
- "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
- "license": "MIT"
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
},
"node_modules/classnames": {
"version": "2.5.1",
@@ -1444,128 +1759,87 @@
"dev": true,
"license": "MIT"
},
- "node_modules/d3-color": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
- "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-dispatch": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
- "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-drag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
- "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
- "license": "ISC",
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-selection": "3"
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
- "node_modules/d3-ease": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
- "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
- "license": "BSD-3-Clause",
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
- "node_modules/d3-interpolate": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
- "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
- "license": "ISC",
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "d3-color": "1 - 3"
+ "ms": "^2.1.3"
},
"engines": {
- "node": ">=12"
- }
- },
- "node_modules/d3-selection": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
- "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
}
},
- "node_modules/d3-timer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
- "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/d3-transition": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
- "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
- "license": "ISC",
- "dependencies": {
- "d3-color": "1 - 3",
- "d3-dispatch": "1 - 3",
- "d3-ease": "1 - 3",
- "d3-interpolate": "1 - 3",
- "d3-timer": "1 - 3"
- },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
"engines": {
- "node": ">=12"
- },
- "peerDependencies": {
- "d3-selection": "2 - 3"
+ "node": ">=6"
}
},
- "node_modules/d3-zoom": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
- "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
- "license": "ISC",
- "dependencies": {
- "d3-dispatch": "1 - 3",
- "d3-drag": "2 - 3",
- "d3-interpolate": "1 - 3",
- "d3-selection": "2 - 3",
- "d3-transition": "2 - 3"
- },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
"engines": {
- "node": ">=12"
+ "node": ">=6"
}
},
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
+ "peer": true
},
"node_modules/electron-to-chromium": {
"version": "1.5.368",
@@ -1574,6 +1848,26 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -1626,6 +1920,26 @@
"node": ">=6"
}
},
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1669,12 +1983,113 @@
"node": ">=6.9.0"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -1713,6 +2128,13 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -1723,6 +2145,27 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1759,6 +2202,43 @@
"node": ">=18"
}
},
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1808,6 +2288,32 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -1833,6 +2339,14 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -1902,6 +2416,33 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -1921,6 +2462,13 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1931,6 +2479,61 @@
"node": ">=0.10.0"
}
},
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -1948,6 +2551,82 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -1979,15 +2658,6 @@
"browserslist": ">= 4.21.0"
}
},
- "node_modules/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/vite": {
"version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
@@ -2063,40 +2733,225 @@
}
}
},
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
"dev": true,
- "license": "ISC"
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
},
- "node_modules/zustand": {
- "version": "4.5.7",
- "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
- "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "node_modules/vitest": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
+ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "use-sync-external-store": "^1.2.2"
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.7",
+ "@vitest/mocker": "3.2.7",
+ "@vitest/pretty-format": "^3.2.7",
+ "@vitest/runner": "3.2.7",
+ "@vitest/snapshot": "3.2.7",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
},
"engines": {
- "node": ">=12.7.0"
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@types/react": ">=16.8",
- "immer": ">=9.0.6",
- "react": ">=16.8"
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.7",
+ "@vitest/ui": "3.2.7",
+ "happy-dom": "*",
+ "jsdom": "*"
},
"peerDependenciesMeta": {
- "@types/react": {
+ "@edge-runtime/vm": {
"optional": true
},
- "immer": {
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz",
+ "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
"optional": true
},
- "react": {
+ "utf-8-validate": {
"optional": true
}
}
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
}
}
}
diff --git a/workshops/agent_arena/web/package.json b/workshops/agent_arena/web/package.json
index 39a4e3a..7997350 100644
--- a/workshops/agent_arena/web/package.json
+++ b/workshops/agent_arena/web/package.json
@@ -1,23 +1,25 @@
{
- "name": "chatbi-arena-diagram",
+ "name": "agent-arena-dashboard",
"private": true,
"version": "1.0.0",
"type": "module",
- "description": "Animated React Flow data-flow diagram of the AgentArena architecture",
+ "description": "AgentArena leaderboard and chat dashboard",
"scripts": {
"dev": "vite",
"build": "vite build",
- "preview": "vite preview --port 4173"
+ "preview": "vite preview --port 4173",
+ "test": "vitest run"
},
"dependencies": {
- "@dagrejs/dagre": "^1.1.8",
- "@xyflow/react": "^12.3.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-tooltip": "^5.30.1"
},
"devDependencies": {
+ "@testing-library/react": "^16.3.2",
"@vitejs/plugin-react": "^4.3.4",
- "vite": "^6.0.7"
+ "jsdom": "^26.1.0",
+ "vite": "^6.0.7",
+ "vitest": "^3.2.7"
}
}
diff --git a/workshops/agent_arena/web/src/App.jsx b/workshops/agent_arena/web/src/App.jsx
index 4f31480..2dd041b 100644
--- a/workshops/agent_arena/web/src/App.jsx
+++ b/workshops/agent_arena/web/src/App.jsx
@@ -1,46 +1,19 @@
-import { useEffect, useState } from 'react'
-import FlowDiagram from './diagram/FlowDiagram.jsx'
+import { useState } from 'react'
import Leaderboard from './leaderboard/Leaderboard.jsx'
-import Countdown from './countdown/Countdown.jsx'
import Chat from './chat/Chat.jsx'
import { BrandLock, Icon } from './ui.jsx'
const NAV = [
- { id: 'countdown', label: 'Countdown', icon: 'clock' },
- { id: 'architecture', label: 'Architecture', icon: 'flow' },
{ id: 'leaderboard', label: 'Leaderboard', icon: 'trophy' },
{ id: 'chat', label: 'Chat', icon: 'bolt' },
]
-function fmtClock(s) {
- s = Math.max(0, s)
- return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
-}
-
export default function App() {
- const [tab, setTab] = useState('countdown')
- // mount each view the first time its tab is shown (so React Flow's fitView
- // measures a real container); keep them mounted after to preserve state.
- const [visited, setVisited] = useState({ countdown: true })
+ const [tab, setTab] = useState('leaderboard')
+ // Keep a view mounted after its first visit so learner interactions persist.
+ const [visited, setVisited] = useState({ leaderboard: true })
const goTab = (id) => { setTab(id); setVisited((v) => ({ ...v, [id]: true })) }
- // ---- timer (owned here so it keeps running across tab switches) ----
- const [duration, setDuration] = useState(600)
- const [remaining, setRemaining] = useState(600)
- const [running, setRunning] = useState(false)
- useEffect(() => {
- if (!running) return
- const t = setInterval(() => setRemaining((r) => {
- if (r <= 1) { clearInterval(t); setRunning(false); return 0 }
- return r - 1
- }), 1000)
- return () => clearInterval(t)
- }, [running])
- const onPreset = (sec) => { setRunning(false); setDuration(sec); setRemaining(sec) }
- const onToggle = () => { if (remaining <= 0) { setRemaining(duration); setRunning(false) } else setRunning((r) => !r) }
- const onReset = () => { setRunning(false); setRemaining(duration) }
- const cdProps = { duration, remaining, running, onPreset, onToggle, onReset }
-
return (
-
-
-
- {visited.architecture && (
-
-
-
- )}
{visited.leaderboard && (
diff --git a/workshops/agent_arena/web/src/assets/arena.jpg b/workshops/agent_arena/web/src/assets/arena.jpg
deleted file mode 100644
index 0b0b3a6..0000000
Binary files a/workshops/agent_arena/web/src/assets/arena.jpg and /dev/null differ
diff --git a/workshops/agent_arena/web/src/countdown/Countdown.jsx b/workshops/agent_arena/web/src/countdown/Countdown.jsx
deleted file mode 100644
index 794a500..0000000
--- a/workshops/agent_arena/web/src/countdown/Countdown.jsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { useEffect, useMemo, useState } from 'react'
-import './countdown.css'
-import arenaImg from '../assets/arena.jpg'
-import { api } from '../api.js'
-import { FAMILIES, FIGHTER_FAMILIES, famKeyOf } from '../meta.js'
-import { Icon } from '../ui.jsx'
-
-function Embers() {
- const els = useMemo(() => Array.from({ length: 26 }, () => ({
- left: Math.random() * 100, dur: 7 + Math.random() * 9, delay: -Math.random() * 12, size: 2 + Math.random() * 2.5,
- })), [])
- return (
-
- {els.map((e, i) => (
-
- ))}
-
- )
-}
-
-function Sparks() {
- const els = useMemo(() => Array.from({ length: 16 }, () => {
- const a = Math.random() * Math.PI * 2, d = 40 + Math.random() * 120
- return { dx: Math.cos(a) * d, dy: Math.sin(a) * d, delay: -Math.random() * 1.6 }
- }), [])
- return <>{els.map((s, i) =>
)}>
-}
-
-function Confetti() {
- const els = useMemo(() => Array.from({ length: 90 }, () => ({
- left: Math.random() * 100, dur: 2.4 + Math.random() * 2.4, delay: Math.random() * 2,
- c: ['var(--accent)', 'var(--fam-claude)', 'var(--fam-qwen)', 'var(--fam-deepseek)', 'var(--fam-kimi)', '#fff'][Math.floor(Math.random() * 6)],
- })), [])
- return (
-
- {els.map((e, i) => )}
-
- )
-}
-
-export default function Countdown({ duration, remaining, running, onPreset, onToggle, onReset }) {
- const [bets, setBets] = useState(() => new Set())
- // best execution accuracy per family, from the most recent run (optional flair)
- const [bestByFam, setBestByFam] = useState({})
- useEffect(() => {
- let alive = true
- api('/api/runs')
- .then((runs) => (runs && runs.length ? api(`/api/leaderboard?run_id=${encodeURIComponent(runs[0])}`) : []))
- .then((board) => {
- if (!alive) return
- const best = {}
- board.forEach((r) => {
- const k = famKeyOf(r.model_name)
- const acc = Number(r.accuracy) * 100
- if (!(k in best) || acc > best[k]) best[k] = acc
- })
- setBestByFam(best)
- })
- .catch(() => {})
- return () => { alive = false }
- }, [])
-
- const glints = useMemo(() => Array.from({ length: 10 }, () => ({
- left: 15 + Math.random() * 70, top: 30 + Math.random() * 50, dur: 1.4 + Math.random() * 1.8, delay: -Math.random() * 2.5,
- })), [])
-
- const live = remaining <= 0
- const mm = String(Math.floor(Math.max(0, remaining) / 60)).padStart(2, '0')
- const ss = String(Math.max(0, remaining) % 60).padStart(2, '0')
- const digits = [mm[0], mm[1], ':', ss[0], ss[1]]
- const toggleBet = (k) => setBets((prev) => { const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k); return n })
-
- return (
-
-
- {live &&
}
-
- {/* LEFT (≈70%): title + the arena image */}
-
-
-
◆ AgentArena · Live NL→SQL Contest ◆
-
- {live ? <>⚔ The Arena is Live ⚔> : <>Guess who will win the Arena?>}
-
-
-
- {/* stage — the original AI-generated arena, framed, with sword/beam VFX */}
-
-

-
-
-
-
- {glints.map((g, i) => (
-
- ))}
-
-
-
-
- {/* RIGHT (≈30%): the countdown timer + presenter controls */}
-
- {/* timer */}
-
-
{live ? 'Fight!' : 'Demo starts in'}
-
- {digits.map((d, i) => d === ':' ? : : {d})}
-
-
-
- {/* bets */}
-
- {FIGHTER_FAMILIES.map((f) => {
- const fam = FAMILIES[f]
- return
- })}
-
-
- {bets.size
- ? <>Your pick{bets.size > 1 ? 's' : ''}: {[...bets].map((b) => FAMILIES[b].label).join(', ')} 👑>
- : <>Place your bets — who takes the crown? 👑>}
-
- {Object.keys(bestByFam).length > 0 && (
-
- Top so far —{' '}
- {FIGHTER_FAMILIES.filter((f) => bestByFam[f] != null).map((f, i, arr) => (
- {FAMILIES[f].label} {bestByFam[f].toFixed(0)}%{i < arr.length - 1 ? ' · ' : ''}
- ))}
-
- )}
-
- {/* presenter controls */}
-
- {[5, 10, 15].map((m) => (
-
- ))}
-
-
-
-
-
{/* /cd-right */}
-
- )
-}
diff --git a/workshops/agent_arena/web/src/countdown/countdown.css b/workshops/agent_arena/web/src/countdown/countdown.css
deleted file mode 100644
index 580e752..0000000
--- a/workshops/agent_arena/web/src/countdown/countdown.css
+++ /dev/null
@@ -1,218 +0,0 @@
-/* Countdown — the live-event "stage" screen. Unified with the ClickHouse brand:
- near-black + neon yellow, Space Grotesk display, mono timer. */
-
-.cd {
- position: relative; z-index: 1;
- height: 100%;
- display: flex; flex-direction: row; align-items: stretch;
- overflow: hidden;
-}
-/* LEFT ≈70%: title + arena image */
-.cd-left {
- flex: 0 0 70%; min-width: 0;
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- gap: 20px; padding: 30px 28px 34px 40px;
-}
-.cd-titleblock { flex: none; display: flex; flex-direction: column; align-items: center; gap: 6px; }
-/* RIGHT ≈30%: timer + presenter controls */
-.cd-right {
- flex: 0 0 30%; min-width: 280px;
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- gap: 22px; padding: 30px 40px 34px 28px;
- background: color-mix(in oklab, var(--bg-1) 55%, transparent);
-}
-/* stack on narrow screens (phones / split windows) */
-@media (max-width: 960px) {
- .cd { flex-direction: column; align-items: center; overflow-y: auto; padding-bottom: 28px; }
- .cd-left, .cd-right { flex: none; width: 100%; }
- .cd-right { background: transparent; }
- .cd-stage { flex: none; height: clamp(280px, 42vh, 440px); }
-}
-.cd-embers { position: fixed; inset: 0; pointer-events: none; z-index: 0; }
-.cd-ember {
- position: absolute; bottom: -10px; width: 3px; height: 3px; border-radius: 50%;
- background: var(--accent); box-shadow: 0 0 8px 1px var(--accent);
- animation: emberRise linear infinite;
- opacity: 0;
-}
-@keyframes emberRise {
- 0% { transform: translateY(0) scale(1); opacity: 0; }
- 12% { opacity: .9; }
- 100% { transform: translateY(-98vh) scale(.3); opacity: 0; }
-}
-
-.cd-kicker {
- font-family: var(--f-mono); font-size: 12px; font-weight: 600;
- letter-spacing: 0.34em; text-transform: uppercase; color: var(--ink-3);
- display: flex; align-items: center; gap: 12px;
-}
-.cd-kicker .dmd { color: var(--accent); }
-
-.cd-head {
- font-family: var(--f-display); font-weight: 700;
- font-size: clamp(28px, 3.4vw, 56px); line-height: 1.02;
- text-align: center; letter-spacing: -0.02em; text-transform: uppercase;
- margin: 0;
-}
-.cd-head .glow { color: var(--accent); text-shadow: 0 0 38px var(--accent-glow), 0 0 12px var(--accent-glow); }
-.cd.live .cd-head { animation: liveFlash 1.1s ease-in-out infinite; }
-@keyframes liveFlash { 0%,100% { filter: brightness(1); } 50% { filter: brightness(1.35); } }
-
-/* ---------------- the arena stage ---------------- */
-.cd-stage {
- position: relative; width: 100%; flex: 1 1 auto; min-height: 0;
- border-radius: var(--r-xl);
- border: 1px solid var(--line-2);
- background:
- radial-gradient(120% 90% at 50% 8%, color-mix(in oklab, var(--accent) 10%, transparent), transparent 55%),
- radial-gradient(80% 120% at 50% 120%, #05060a, #0a0d12 60%, #0b0f15);
- box-shadow: var(--sh-3), inset 0 0 0 1px rgba(255,255,255,.03), 0 0 0 6px color-mix(in oklab, var(--accent) 7%, transparent);
- overflow: hidden;
-}
-/* the original AI-generated arena image, filling the framed stage */
-.cd-arena-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
-
-/* VFX layer sits exactly over the image: sword sparks + saber beams + clash bloom */
-.cd-vfx { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
-.cd-vfx .clash {
- position: absolute; left: 50%; top: 53%; width: clamp(160px, 28%, 240px); aspect-ratio: 1;
- transform: translate(-50%,-50%); mix-blend-mode: screen;
- background: radial-gradient(circle, rgba(255,255,255,.55) 0%, rgba(255,214,106,.35) 22%, rgba(255,138,30,.12) 45%, transparent 65%);
- animation: clashPulse 1.7s ease-in-out infinite;
-}
-@keyframes clashPulse { 0%,100% { opacity: .45; transform: translate(-50%,-50%) scale(.85); } 50% { opacity: 1; transform: translate(-50%,-50%) scale(1.12); } }
-.cd-vfx .beam {
- position: absolute; left: 50%; top: 53%; width: 46%; height: 5px; transform-origin: left center;
- border-radius: 4px; mix-blend-mode: screen; filter: blur(1px); animation: beamPulse 1.7s ease-in-out infinite;
-}
-.cd-vfx .beam.b1 { transform: translate(0,-50%) rotate(-32deg); background: linear-gradient(90deg, rgba(120,180,255,.9), rgba(120,180,255,0)); }
-.cd-vfx .beam.b2 { transform: translate(0,-50%) rotate(212deg); background: linear-gradient(90deg, rgba(255,170,70,.9), rgba(255,170,70,0)); }
-.cd-vfx .beam.b3 { transform: translate(0,-50%) rotate(-150deg); background: linear-gradient(90deg, rgba(180,120,255,.85), rgba(180,120,255,0)); }
-.cd-vfx .beam.b4 { transform: translate(0,-50%) rotate(28deg); background: linear-gradient(90deg, rgba(80,230,140,.85), rgba(80,230,140,0)); }
-.cd-glint {
- position: absolute; width: 3px; height: 3px; border-radius: 50%; background: #fff;
- box-shadow: 0 0 8px 2px #cfe6ff; opacity: 0;
- animation-name: twinkle; animation-timing-function: ease-in-out; animation-iteration-count: infinite;
-}
-@keyframes twinkle { 0%,100% { opacity: 0; transform: scale(.5); } 50% { opacity: 1; transform: scale(1.3); } }
-
-/* "Top so far" leaders line under the bets */
-.cd-leaders { font-family: var(--f-mono); font-size: 12px; color: var(--ink-3); letter-spacing: .02em; }
-.cd-leaders b { font-weight: 700; }
-
-/* perspective data-grid floor (unused now the arena image is the centerpiece) */
-.cd-floor {
- position: absolute; left: -20%; right: -20%; bottom: -2%; height: 56%;
- background-image:
- linear-gradient(color-mix(in oklab, var(--accent) 30%, transparent) 1px, transparent 1px),
- linear-gradient(90deg, color-mix(in oklab, var(--accent) 30%, transparent) 1px, transparent 1px);
- background-size: 46px 46px;
- transform: perspective(520px) rotateX(62deg);
- transform-origin: bottom center;
- mask: radial-gradient(120% 100% at 50% 0%, #000 18%, transparent 78%);
- opacity: .5;
- animation: floorScroll 6s linear infinite;
-}
-@keyframes floorScroll { from { background-position: 0 0; } to { background-position: 0 46px; } }
-
-/* arena ring */
-.cd-ring {
- position: absolute; left: 50%; top: 50%; transform: translate(-50%,-44%);
- width: 58%; aspect-ratio: 1; border-radius: 50%;
- border: 1px solid color-mix(in oklab, var(--accent) 30%, transparent);
- box-shadow: 0 0 60px -10px var(--accent-glow), inset 0 0 50px -20px var(--accent-glow);
-}
-.cd-ring::after {
- content: ""; position: absolute; inset: 14%; border-radius: 50%;
- border: 1px dashed color-mix(in oklab, var(--accent) 22%, transparent);
-}
-
-/* central core (ClickHouse pedestal + beam) */
-.cd-core { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-58%); display: flex; flex-direction: column; align-items: center; gap: 10px; z-index: 4; }
-.cd-beam {
- position: absolute; left: 50%; top: -160px; transform: translateX(-50%);
- width: 80px; height: 200px;
- background: linear-gradient(to top, var(--accent-glow), transparent);
- filter: blur(8px); opacity: .8;
- clip-path: polygon(38% 100%, 62% 100%, 100% 0, 0 0);
- animation: beamPulse 2.2s ease-in-out infinite;
-}
-@keyframes beamPulse { 0%,100% { opacity:.45; } 50% { opacity:.95; } }
-.cd-pedestal {
- display: flex; align-items: flex-end; gap: 4px; padding: 12px 14px;
- border-radius: 12px; background: rgba(8,10,14,.7); border: 1px solid var(--line-2);
- box-shadow: 0 0 50px -12px var(--accent-glow);
-}
-.cd-core-label {
- font-family: var(--f-mono); font-size: 11px; font-weight: 600; letter-spacing: .12em;
- color: var(--accent-ink); background: var(--accent); padding: 3px 10px; border-radius: 999px;
-}
-
-/* fighters */
-.cd-fighter {
- position: absolute; z-index: 5; transform: translate(-50%, -50%);
- display: flex; flex-direction: column; align-items: center; gap: 7px;
- transition: transform .25s;
-}
-.cd-fighter:hover { transform: translate(-50%, -54%) scale(1.06); z-index: 8; }
-.cd-avatar {
- --c: var(--accent);
- width: 54px; height: 54px; border-radius: 16px;
- display: grid; place-items: center;
- background: radial-gradient(120% 120% at 30% 20%, color-mix(in oklab, var(--c) 36%, #0a0d12), #0a0d12);
- border: 1.5px solid var(--c);
- box-shadow: 0 0 26px -6px var(--c), inset 0 0 16px -8px var(--c);
- font-family: var(--f-display); font-weight: 700; font-size: 22px; color: var(--c);
- animation: bob 3.4s ease-in-out infinite;
-}
-@keyframes bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
-.cd-namecard {
- background: rgba(8,10,14,.82); border: 1px solid var(--line-2); border-radius: 9px;
- padding: 4px 9px; text-align: center; min-width: 78px; backdrop-filter: blur(4px);
-}
-.cd-namecard .nm { font-weight: 700; font-size: 12px; }
-.cd-namecard .st { font-family: var(--f-mono); font-size: 9.5px; color: var(--ink-3); letter-spacing: .02em; }
-
-/* sparks */
-.cd-spark { position: absolute; left: 50%; top: 50%; width: 3px; height: 3px; border-radius: 50%; background: #fff; box-shadow: 0 0 8px 2px var(--accent); animation: spark 1.6s ease-out infinite; }
-@keyframes spark { 0% { transform: translate(0,0) scale(1); opacity: 1; } 100% { transform: translate(var(--dx), var(--dy)) scale(0); opacity: 0; } }
-
-/* ---------------- timer ---------------- */
-.cd-timerwrap { display: flex; flex-direction: column; align-items: center; gap: 8px; }
-.cd-timerlabel {
- font-family: var(--f-mono); font-weight: 700; letter-spacing: .32em;
- font-size: clamp(20px, 2.5vw, 36px); color: var(--accent);
- text-transform: uppercase; white-space: nowrap;
- text-shadow: 0 0 10px var(--accent), 0 0 26px var(--accent-glow), 0 0 54px var(--accent-glow);
- animation: labelGlow 1.8s ease-in-out infinite;
-}
-@keyframes labelGlow {
- 0%, 100% { text-shadow: 0 0 8px var(--accent-glow), 0 0 24px var(--accent-glow); }
- 50% { text-shadow: 0 0 14px var(--accent), 0 0 34px var(--accent-glow), 0 0 64px var(--accent-glow); }
-}
-.cd-timer { display: flex; align-items: center; gap: 6px; }
-.cd-digit {
- font-family: var(--f-mono); font-weight: 700; font-variant-numeric: tabular-nums;
- font-size: clamp(30px, 4.4vw, 62px); line-height: 1;
- background: linear-gradient(180deg, #fff, #cfd6e0);
- -webkit-background-clip: text; background-clip: text; color: transparent;
- padding: 4px 9px; border-radius: 12px;
- border: 1px solid var(--line); background-color: rgba(255,255,255,.02);
- box-shadow: inset 0 -10px 30px -20px #fff;
-}
-.cd-colon { font-family: var(--f-mono); font-weight: 700; font-size: clamp(24px, 3.4vw, 48px); color: var(--accent); animation: blink 1s steps(1) infinite; }
-@keyframes blink { 50% { opacity: .25; } }
-.cd.live .cd-digit { background: linear-gradient(180deg, var(--accent), var(--accent-2)); -webkit-background-clip: text; background-clip: text; }
-
-/* badges + bets */
-.cd-badges { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
-.cd-badge { --c: var(--accent); display: inline-flex; align-items: center; gap: 7px; padding: 7px 14px; border-radius: 999px; border: 1px solid var(--c); color: var(--c); font-weight: 700; font-size: 13px; background: color-mix(in oklab, var(--c) 12%, #0a0d12); cursor: pointer; transition: all .14s; }
-.cd-badge:hover, .cd-badge[data-bet="true"] { background: var(--c); color: #08090c; box-shadow: 0 0 22px -6px var(--c); }
-.cd-bets { color: var(--ink-2); font-size: 14px; }
-.cd-controls { display: flex; align-items: center; gap: 8px; }
-.cd-sep { width: 1px; height: 24px; background: var(--line-2); margin: 0 4px; }
-
-/* confetti */
-.cd-confetti { position: fixed; inset: 0; pointer-events: none; z-index: 50; }
-.cd-conf { position: absolute; top: -12px; width: 8px; height: 12px; border-radius: 2px; animation: confFall linear forwards; }
-@keyframes confFall { to { transform: translateY(108vh) rotate(720deg); opacity: .5; } }
diff --git a/workshops/agent_arena/web/src/dashboard.test.jsx b/workshops/agent_arena/web/src/dashboard.test.jsx
new file mode 100644
index 0000000..ee732c8
--- /dev/null
+++ b/workshops/agent_arena/web/src/dashboard.test.jsx
@@ -0,0 +1,78 @@
+/** @vitest-environment jsdom */
+import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
+import App from './App.jsx'
+import Leaderboard from './leaderboard/Leaderboard.jsx'
+import { api } from './api.js'
+
+vi.mock('./api.js', () => ({
+ API_BASE: 'http://api.test',
+ SERVING_BASE: 'http://serving.test',
+ api: vi.fn(),
+}))
+
+function deferred() {
+ let resolve
+ const promise = new Promise((done) => { resolve = done })
+ return { promise, resolve }
+}
+
+function resolvedApi(path) {
+ if (path === '/api/runs') return Promise.resolve([])
+ if (path === '/api/meta') return Promise.resolve({})
+ if (path === '/api/grid-options') return Promise.resolve({ prompts: [] })
+ return Promise.resolve([])
+}
+
+describe('workshop dashboard surface', () => {
+ beforeEach(() => api.mockImplementation(resolvedApi))
+ afterEach(() => cleanup())
+
+ test('only exposes the leaderboard and chat workshop tabs', async () => {
+ render(
)
+
+ expect(screen.getByRole('button', { name: 'Leaderboard' })).toBeTruthy()
+ expect(screen.getByRole('button', { name: 'Chat' })).toBeTruthy()
+ expect(screen.queryByRole('button', { name: 'Countdown' })).toBeNull()
+ expect(screen.queryByRole('button', { name: 'Architecture' })).toBeNull()
+
+ fireEvent.click(screen.getByRole('button', { name: 'Leaderboard' }))
+ expect(screen.queryByText(/Build the arena/i)).toBeNull()
+ expect(screen.queryByText(/Guided walkthrough/i)).toBeNull()
+ })
+
+ test('shows loading states for the run list and selected run data', async () => {
+ const runs = deferred()
+ const board = deferred()
+ const tiers = deferred()
+ const outcomes = deferred()
+ api.mockImplementation((path) => {
+ if (typeof path !== 'string') return Promise.resolve([])
+ if (path === '/api/runs') return runs.promise
+ if (path === '/api/meta') return Promise.resolve({})
+ if (path === '/api/grid-options') return Promise.resolve({ prompts: [] })
+ if (path.startsWith('/api/leaderboard')) return board.promise
+ if (path.startsWith('/api/tiers')) return tiers.promise
+ if (path.startsWith('/api/outcomes')) return outcomes.promise
+ return Promise.resolve([])
+ })
+
+ render(
)
+ const select = screen.getByRole('combobox')
+ expect(select.disabled).toBe(true)
+ expect(screen.getByRole('option', { name: 'Loading runs…' })).toBeTruthy()
+
+ await act(async () => { runs.resolve(['actual-model-run']) })
+ expect(await screen.findByText('Loading dashboard data')).toBeTruthy()
+ expect(select.disabled).toBe(true)
+
+ await act(async () => {
+ board.resolve([])
+ tiers.resolve([])
+ outcomes.resolve([])
+ })
+ await waitFor(() => expect(screen.queryByText('Loading dashboard data')).toBeNull())
+ expect(select.disabled).toBe(false)
+ expect(screen.getByRole('option', { name: 'actual-model-run' })).toBeTruthy()
+ })
+})
diff --git a/workshops/agent_arena/web/src/diagram/FlowDiagram.jsx b/workshops/agent_arena/web/src/diagram/FlowDiagram.jsx
deleted file mode 100644
index 22b5bc6..0000000
--- a/workshops/agent_arena/web/src/diagram/FlowDiagram.jsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useMemo } from 'react'
-import {
- ReactFlow, Background, Controls, MiniMap, Panel, MarkerType,
- useNodesState, useEdgesState,
-} from '@xyflow/react'
-import '@xyflow/react/dist/style.css'
-
-import { componentNodes, edges as rawEdges, EDGE_COLORS, ENV } from './graph.js'
-import { layoutLR } from './layout.js'
-import CardNode from './nodes/CardNode.jsx'
-import AnimatedFlowEdge from './edges/AnimatedFlowEdge.jsx'
-
-const nodeTypes = { card: CardNode }
-const edgeTypes = { flow: AnimatedFlowEdge }
-
-const FLOW_LEGEND = [
- ['OLTP + CDC data', EDGE_COLORS.data],
- ['OpenRouter inference', EDGE_COLORS.ai],
- ['read-only query', EDGE_COLORS.read],
- ['orchestration', EDGE_COLORS.control],
- ['LangFuse traces', EDGE_COLORS.trace],
-]
-
-export default function FlowDiagram() {
- const initialNodes = useMemo(() => layoutLR(componentNodes, rawEdges), [])
- const initialEdges = useMemo(
- () => rawEdges.map((e) => ({
- ...e,
- markerEnd: { type: MarkerType.ArrowClosed, color: e.data.color, width: 15, height: 15 },
- })),
- [],
- )
-
- const [nodes, , onNodesChange] = useNodesState(initialNodes)
- const [edges, , onEdgesChange] = useEdgesState(initialEdges)
-
- return (
-
-
-
- n.data?.accent || '#888'}
- maskColor="rgba(14,17,22,0.7)" style={{ background: '#161b22' }} />
-
-
- Runs on
- {Object.values(ENV).map((envv) => (
-
-
- {envv.label}
-
- ))}
- Flow type
- {FLOW_LEGEND.map(([label, color]) => (
-
-
- {label}
-
- ))}
-
-
- )
-}
diff --git a/workshops/agent_arena/web/src/diagram/edges/AnimatedFlowEdge.jsx b/workshops/agent_arena/web/src/diagram/edges/AnimatedFlowEdge.jsx
deleted file mode 100644
index fdafb78..0000000
--- a/workshops/agent_arena/web/src/diagram/edges/AnimatedFlowEdge.jsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from '@xyflow/react'
-
-// A data-flow edge: a faint orthogonal base path, a moving dashed overlay, and a
-// packet (circle) traveling along it to convey direction. Smoothstep routing
-// keeps edges in clean horizontal/vertical lanes (far fewer visual crossings
-// than beziers).
-export default function AnimatedFlowEdge({
- id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition,
- label, data, markerEnd,
-}) {
- const [path, labelX, labelY] = getSmoothStepPath({
- sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition,
- borderRadius: 12,
- })
- const color = data?.color || '#8b96a8'
-
- return (
- <>
-
-
-
-
-
- {label && (
-
-
- {label}
- {data?.detail && {data.detail}}
-
-
- )}
- >
- )
-}
diff --git a/workshops/agent_arena/web/src/diagram/graph.js b/workshops/agent_arena/web/src/diagram/graph.js
deleted file mode 100644
index 13f7449..0000000
--- a/workshops/agent_arena/web/src/diagram/graph.js
+++ /dev/null
@@ -1,59 +0,0 @@
-// The AgentArena architecture as React Flow nodes + animated edges.
-// Positions are computed by dagre (layout.js); nodes only declare a size + env.
-
-export const EDGE_COLORS = {
- data: '#3fb950', // seed writes
- ai: '#a371f7', // OpenRouter inference
- read: '#f5d90a', // read-only analytic queries
- control: '#58a6ff', // orchestration / requests
- trace: '#ec6cb9', // LangFuse traces + scores
-}
-
-// Where each component runs -> card accent color (legend, not a bounding box).
-export const ENV = {
- local: { label: 'Local / Docker', color: '#58a6ff' },
- ch: { label: 'ClickHouse Cloud', color: '#f5d90a' },
- saas: { label: 'External SaaS (LangFuse · OpenRouter)', color: '#a371f7' },
-}
-
-const card = (id, env, title, subtitle, kind, w = 215) => ({
- id, type: 'card', position: { x: 0, y: 0 }, width: w, height: 66,
- data: { title, subtitle, kind, env, accent: ENV[env].color },
-})
-
-export const componentNodes = [
- card('user', 'local', 'Analyst', 'asks a question', 'user', 170),
- card('datagen', 'local', 'Data generator', 'Faker · seed + mutations', 'job'),
- card('golden', 'local', 'Golden set', '18 Qs × 5 tiers + golden SQL', 'data'),
- card('serving', 'local', 'Serving API', 'FastAPI · POST /ask', 'api'),
- card('harness', 'local', 'Benchmark harness', 'grid runner · emits experiment items', 'job'),
- card('agent', 'local', 'Agent core', 'loop · P1–P3 · SQL guard', 'agent', 230),
- card('dashboard', 'local', 'Leaderboard dashboard', 'FastAPI · LangFuse Public API', 'api'),
-
- card('openrouter', 'saas', 'OpenRouter', 'OpenAI-compatible · Claude/GPT-4o/Gemini/DeepSeek/Qwen/Llama', 'ai', 300),
-
- card('views', 'ch', 'v_* analytic views', 'FINAL · dedup current state', 'view', 300),
- card('langfuse', 'saas', 'LangFuse Cloud', 'experiments · results · scores · traces', 'trace', 300),
-]
-
-// All edges flow forward in the dagre LR ranking, so source=right, target=left.
-// label = short (always shown); detail = full text (shown on hover).
-const e = (id, source, target, label, detail, color) => ({
- id, source, target, label, type: 'flow',
- sourceHandle: 's-right', targetHandle: 't-left',
- data: { color: EDGE_COLORS[color], detail },
-})
-
-export const edges = [
- e('datagen-views', 'datagen', 'views', 'seed', 'direct INSERT into ClickHouse seed tables → v_* views', 'data'),
-
- e('agent-openrouter', 'agent', 'openrouter', 'chat/completions', 'OpenRouter: NL→SQL + token usage', 'ai'),
- e('agent-views', 'agent', 'views', 'SELECT', 'read-only SELECT (sandboxed)', 'read'),
- e('golden-harness', 'golden', 'harness', 'Qs', 'questions + golden SQL', 'control'),
- e('harness-agent', 'harness', 'agent', 'run', 'run grid: model × prompt', 'control'),
- e('user-serving', 'user', 'serving', '/ask', 'POST /ask', 'control'),
- e('serving-agent', 'serving', 'agent', 'reuse', 'reuses the agent core', 'control'),
-
- e('harness-langfuse', 'harness', 'langfuse', 'store', 'Experiment Items with result, cost and latency; evaluators attach scores', 'trace'),
- e('langfuse-dashboard', 'langfuse', 'dashboard', 'Public API', 'experiments, item outputs, scores and conversations', 'trace'),
-]
diff --git a/workshops/agent_arena/web/src/diagram/layout.js b/workshops/agent_arena/web/src/diagram/layout.js
deleted file mode 100644
index d084c3e..0000000
--- a/workshops/agent_arena/web/src/diagram/layout.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import dagre from '@dagrejs/dagre'
-
-// Layered left->right layout with crossing minimization (dagre's barycenter
-// ordering). Returns nodes with computed positions.
-export function layoutLR(nodes, edges) {
- const g = new dagre.graphlib.Graph()
- g.setGraph({ rankdir: 'LR', nodesep: 46, ranksep: 130, marginx: 50, marginy: 50, ranker: 'tight-tree' })
- g.setDefaultEdgeLabel(() => ({}))
-
- nodes.forEach((n) => g.setNode(n.id, { width: n.width, height: n.height }))
- edges.forEach((e) => g.setEdge(e.source, e.target))
-
- dagre.layout(g)
-
- return nodes.map((n) => {
- const p = g.node(n.id)
- return { ...n, position: { x: p.x - n.width / 2, y: p.y - n.height / 2 } }
- })
-}
diff --git a/workshops/agent_arena/web/src/diagram/nodes/CardNode.jsx b/workshops/agent_arena/web/src/diagram/nodes/CardNode.jsx
deleted file mode 100644
index 65edc24..0000000
--- a/workshops/agent_arena/web/src/diagram/nodes/CardNode.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Handle, Position } from '@xyflow/react'
-
-const ICONS = {
- user: '🧑💻', api: '🛰️', job: '⚙️', agent: '🤖', data: '📄',
- otel: '📡', db: '🗄️', ai: '🧠', view: '🔎', trace: '📊',
-}
-
-const SIDES = [
- ['s-left', 'source', Position.Left], ['t-left', 'target', Position.Left],
- ['s-right', 'source', Position.Right], ['t-right', 'target', Position.Right],
- ['s-top', 'source', Position.Top], ['t-top', 'target', Position.Top],
- ['s-bottom', 'source', Position.Bottom], ['t-bottom', 'target', Position.Bottom],
-]
-
-export default function CardNode({ data }) {
- return (
-
- {SIDES.map(([id, type, position]) => (
-
- ))}
-
{ICONS[data.kind] || '⬡'}
-
-
{data.title}
-
{data.subtitle}
-
-
- )
-}
diff --git a/workshops/agent_arena/web/src/leaderboard/GuidedTour.jsx b/workshops/agent_arena/web/src/leaderboard/GuidedTour.jsx
deleted file mode 100644
index b7dd8e0..0000000
--- a/workshops/agent_arena/web/src/leaderboard/GuidedTour.jsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import { useEffect, useLayoutEffect, useRef, useState } from 'react'
-import { Icon } from '../ui.jsx'
-
-/* A hardcoded, narrated walkthrough of the `open-vs-proprietary1` story
- (docs/open-vs-proprietary-langfuse-story.md). It drives the real Leaderboard
- UI: selects the run, sorts/filters, expands config rows, opens a conversation,
- and spotlights the element each beat is about — the leaderboard tells WHAT,
- the per-question drill-downs + LangFuse tell WHY. `ctl` is the imperative
- handle the Leaderboard hands us. */
-
-const sel = (s) => { try { return document.querySelector(s) } catch { return null } }
-
-export default function GuidedTour({ ctl, initialStep = 0, onStep, onExit }) {
- const ctlRef = useRef(ctl); ctlRef.current = ctl
- const [index, setIndex] = useState(initialStep)
- const [box, setBox] = useState({ hole: null, card: { left: 0, top: 0 } })
- const [link, setLink] = useState(null)
- const [busy, setBusy] = useState(true)
- const targetRef = useRef(null)
- const cardRef = useRef(null)
- const timers = useRef([])
- const addTimer = (fn, ms) => { const id = setTimeout(fn, ms); timers.current.push(id); return id }
- const clearTimers = () => { timers.current.forEach(clearTimeout); timers.current = [] }
-
- // ---- resolve real config_ids from the live board (robust to prompt suffixes) ----
- const board = () => ctlRef.current.getBoard()
- const winner = () => [...board()].sort((a, b) => Number(b.accuracy) - Number(a.accuracy))[0]?.config_id
- const find = (m, p) => board().find((r) => (r.model_name || '').includes(m) && (!p || (r.prompt_name || '').includes(p)))?.config_id
- const rowOf = (cid) => (cid ? sel(`[data-tour-config="${cid}"]`) : null)
- const qRowOf = (cid, qid) => (cid ? sel(`[data-tour-dd="${cid}"] tr[data-q="${qid}"]`) : null)
- const waitBoard = () => new Promise((res) => {
- const t0 = Date.now()
- const tick = () => (board().length || Date.now() - t0 > 4000 ? res() : addTimer(tick, 80))
- tick()
- })
-
- // ---- the hardcoded story ----
- const STEPS = [
- {
- kicker: 'Open vs Proprietary', title: 'A 2-minute read of the arena',
- body: <>6 models × 3 prompts × 18 golden questions =
324 graded NL→SQL attempts on live ClickHouse. The leaderboard tells us
what won; the per-question traces tell us
why. Let’s walk the story.>,
- before: async () => { const c = ctlRef.current; c.selectRun('open-vs-proprietary1'); c.collapseAll(); c.setPrompt('All'); c.setSort('accuracy', -1); await waitBoard() },
- },
- {
- kicker: 'The scoreboard', title: 'Claude Sonnet wins raw accuracy',
- body: <>Sorted by accuracy,
Claude Sonnet 4.5 tops the board at
83% (15/18). That’s the headline number — but raw accuracy isn’t the whole story.>,
- target: () => rowOf(winner()), wait: 4000,
- },
- {
- kicker: 'The value champion', title: 'Open-weight Qwen3-235b is the value pick',
- body: <>Watch the
$/correct column:
Qwen3-235b is one question behind Sonnet but at
~6× lower cost per correct answer — and faster. Open beats proprietary on value.>,
- target: () => rowOf(find('qwen')), wait: 4000,
- },
- {
- kicker: 'Cost × accuracy', title: 'See it in one chart',
- body: <>Qwen sits in the
cheap-and-accurate corner;
Opus is worse and pricier than Sonnet; the reasoning models drift up (costly) without moving right (accurate).>,
- target: () => sel('[data-tour="scatter"]'),
- },
- {
- kicker: 'Drill 1 · why', title: 'All six models “failed” q007',
- body: <>Expand a config and find
q007 — every model got it wrong. Six identical wrong answers is the tell: they all counted cancelled/returned orders. The
question never said to exclude them — an ambiguous golden, not a model failure.>,
- before: async () => { const cid = find('opus') || winner(); if (cid) ctlRef.current.openOnly(cid) },
- target: () => qRowOf(find('opus') || winner(), 'q007'), wait: 5000,
- },
- {
- kicker: 'Drill 2 · open wins (1/2)', title: 'q015 — Claude got it wrong',
- body: <>Open a
Claude config at
q015 — it’s red. Claude read “last 14 days” as
exclusive of today and returned 14 days; the golden expected 15. A defensible reading, but off by one boundary day. Open the trace to see the date window.>,
- before: async () => { const cid = find('claude'); if (cid) ctlRef.current.openOnly(cid) },
- target: () => qRowOf(find('claude'), 'q015'), wait: 5000,
- },
- {
- kicker: 'Drill 2 · open wins (2/2)', title: 'q015 — open models nailed it',
- body: <>Now
Qwen3-235b at the same question — green. Qwen and DeepSeek matched the golden’s convention (
inclusive of today). Here the open camp
beat both Claude models outright — another spec ambiguity, only visible by diffing the traces.>,
- before: async () => { const cid = find('qwen'); if (cid) ctlRef.current.openOnly(cid) },
- target: () => qRowOf(find('qwen'), 'q015'), wait: 5000,
- },
- {
- kicker: 'Drill 3 · operational', title: 'Reasoning models: slow & fragile',
- body: <>Sort by latency:
DeepSeek-R1 and
Kimi burn 6–8s — ~20× the tokens of Qwen for
no accuracy gain, and they’re format-fragile (answers get policy-rejected). A poor trade for structured NL→SQL.>,
- before: async () => { ctlRef.current.collapseAll(); ctlRef.current.setSort('avg_latency_ms', -1) },
- target: () => sel('[data-tour-col="avg_latency_ms"]'),
- },
- {
- kicker: 'Drill 4 · trust', title: 'Don’t trust the LLM judge alone',
- body: <>The
LLM judge loved answers that were actually wrong — it scored q007 ~0.9 because the SQL
looks right. Judge = plausibility;
execution accuracy is ground truth. We rank on accuracy.>,
- before: async () => { ctlRef.current.setSort('accuracy', -1) },
- target: () => sel('[data-tour-col="avg_judge_score"]'),
- },
- {
- kicker: 'The prompt dimension', title: 'Prompt strategy barely moved the needle',
- body: <>We’re cycling the prompt filter —
P1 zero-shot → P2 few-shot → P3 dialect. Accuracy hardly changes; the examples and cheat-sheet mostly just added latency.>,
- before: async (_c, add) => {
- const c = ctlRef.current; c.collapseAll(); c.setPrompt('All')
- add(() => c.setPrompt('P1_zeroshot'), 700)
- add(() => c.setPrompt('P2_fewshot'), 1900)
- add(() => c.setPrompt('P3_dialect'), 3100)
- add(() => c.setPrompt('All'), 4300)
- },
- target: () => sel('[data-tour="promptfilter"]'), wait: 1500,
- },
- {
- kicker: 'The film room', title: 'Replay the whole conversation',
- body: <>Every run is a
LangFuse Session. “View conversation” replays a model’s entire pass — watch a model reason through a tricky join, or bury the SQL in verbose thinking. It’s the difference between a number and a behavior.>,
- before: async () => { const cid = find('deepseek') || find('kimi') || winner(); if (cid) { ctlRef.current.openOnly(cid); setTimeout(() => ctlRef.current.openConv(cid), 500) } },
- target: () => sel(`[data-tour-conv="${find('deepseek') || find('kimi') || winner()}"]`), wait: 5000,
- link: () => { const cid = find('deepseek') || find('kimi') || winner(); const u = ctlRef.current.sessionUrl?.(cid); return u ? { href: u, label: 'Open session in LangFuse' } : null },
- },
- {
- kicker: 'Takeaway', title: 'ClickHouse ran the queries · LangFuse holds the evidence',
- body: <>
Qwen3-235b is the value pick;
Sonnet the accuracy pick; Opus and the reasoning models are poor ROI. And two “failures” (q007, q015) were
bugs in our questions, not the models — invisible on the leaderboard, obvious in the traces.>,
- before: async () => { const c = ctlRef.current; c.collapseAll(); c.setPrompt('All'); c.setSort('accuracy', -1) },
- },
- ]
- const N = STEPS.length
- const step = STEPS[index]
-
- const place = (hole) => {
- const cw = 360, ch = cardRef.current?.offsetHeight || 220, m = 16
- const vw = window.innerWidth, vh = window.innerHeight
- if (!hole) return { left: (vw - cw) / 2, top: (vh - ch) / 2 }
- let top = hole.top + hole.height + m
- if (top + ch > vh - 10) top = hole.top - ch - m
- if (top < 10) top = Math.min(vh - ch - 10, Math.max(10, hole.top))
- let left = hole.left + hole.width / 2 - cw / 2
- left = Math.max(10, Math.min(vw - cw - 10, left))
- return { left, top }
- }
- const measure = () => {
- const el = targetRef.current
- if (!el || !el.isConnected) { setBox({ hole: null, card: place(null) }); return }
- const r = el.getBoundingClientRect()
- const pad = 8
- const hole = { left: r.left - pad, top: r.top - pad, width: r.width + pad * 2, height: r.height + pad * 2 }
- setBox({ hole, card: place(hole) })
- }
-
- // run a step when the index changes
- useEffect(() => {
- let cancelled = false
- clearTimers(); setBusy(true); setLink(null); targetRef.current = null
- ;(async () => {
- try { await step.before?.(ctlRef.current, addTimer) } catch { /* keep going */ }
- let el = null
- if (step.target) {
- const deadline = Date.now() + (step.wait || 1200)
- el = await new Promise((res) => {
- const tick = () => {
- if (cancelled) return res(null)
- let found = null; try { found = step.target() } catch { /* */ }
- if (found) return res(found)
- if (Date.now() > deadline) return res(null)
- addTimer(tick, 70)
- }
- tick()
- })
- }
- if (cancelled) return
- targetRef.current = el
- // surface a clickable link in the card (the scrim blocks the page itself):
- // an explicit step.link, else the "trace ↗" anchor inside the spotlit row.
- let lk = null
- try { lk = step.link ? step.link(ctlRef.current) : null } catch { /* */ }
- if (!lk && el && el.querySelector) {
- const a = el.querySelector('a.trace-link[href]')
- const href = a && a.getAttribute('href')
- if (href && href !== '#') lk = { href: a.href, label: 'Open trace in LangFuse' }
- }
- setLink(lk)
- if (el) el.scrollIntoView({ block: 'center', inline: 'nearest' })
- requestAnimationFrame(() => requestAnimationFrame(() => { if (!cancelled) { measure(); setBusy(false) } }))
- })()
- return () => { cancelled = true }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [index])
-
- // re-place card once it has a real height
- useLayoutEffect(() => { if (!busy) measure() }, [busy]) // eslint-disable-line react-hooks/exhaustive-deps
-
- // keep the spotlight glued on resize; block manual scroll so it can't drift
- useEffect(() => {
- const onResize = () => measure()
- const scroller = document.querySelector('.app-body')
- const block = (e) => e.preventDefault()
- window.addEventListener('resize', onResize)
- scroller?.addEventListener('wheel', block, { passive: false })
- scroller?.addEventListener('touchmove', block, { passive: false })
- return () => {
- window.removeEventListener('resize', onResize)
- scroller?.removeEventListener('wheel', block)
- scroller?.removeEventListener('touchmove', block)
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- // keyboard nav — re-bound per step so it isn't a stale closure
- useEffect(() => {
- const onKey = (e) => {
- if (e.key === 'Escape') exit()
- else if (e.key === 'ArrowRight' || e.key === 'Enter') next()
- else if (e.key === 'ArrowLeft') back()
- }
- window.addEventListener('keydown', onKey)
- return () => window.removeEventListener('keydown', onKey)
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [index])
-
- // remember the step in the parent so closing → reopening resumes here
- useEffect(() => { onStep?.(index) }, [index]) // eslint-disable-line react-hooks/exhaustive-deps
-
- const next = () => (index >= N - 1 ? finish() : setIndex((i) => i + 1))
- const back = () => setIndex((i) => Math.max(0, i - 1))
- // pause: hide the overlay but KEEP the step + the UI state, so you can talk to
- // the audience over the live UI and resume exactly where you left off.
- const exit = () => { clearTimers(); onExit(false) }
- // finish: tour is done — let the parent reset the resume point.
- const finish = () => { clearTimers(); onExit(true) }
-
- return (
- <>
-
e.stopPropagation()} />
- {box.hole &&
}
-
-
-
{step.kicker}
-
{step.title}
-
{step.body}
- {link && (
-
- {link.label}
-
- )}
-
-
{STEPS.map((_, i) => )}
-
- {index + 1}/{N}
-
-
-
-
-
- >
- )
-}
diff --git a/workshops/agent_arena/web/src/leaderboard/Leaderboard.jsx b/workshops/agent_arena/web/src/leaderboard/Leaderboard.jsx
index 86264c4..9e1189a 100644
--- a/workshops/agent_arena/web/src/leaderboard/Leaderboard.jsx
+++ b/workshops/agent_arena/web/src/leaderboard/Leaderboard.jsx
@@ -5,20 +5,11 @@ import './leaderboard.css'
import { api, API_BASE } from '../api.js'
import { famOf, promptMeta, outcomeMeta } from '../meta.js'
import { Icon, ConfigName, FamDot, OutcomeTag, WinPill, Bar, heatColor } from '../ui.jsx'
-import GuidedTour from './GuidedTour.jsx'
const pct = (v) => (v == null ? '—' : (Number(v) * 100).toFixed(1) + '%')
const pct0 = (v) => (v == null ? '—' : (Number(v) * 100).toFixed(0) + '%')
const money = (v, d = 5) => (v == null ? '—' : '$' + Number(v).toFixed(d))
-async function post(path, body) {
- const r = await fetch(API_BASE + path, {
- method: 'POST', headers: { 'content-type': 'application/json' },
- body: JSON.stringify(body),
- })
- if (!r.ok) throw new Error(`${path} → HTTP ${r.status}`)
- return r.json()
-}
function toggleSet(set, v) { const n = new Set(set); n.has(v) ? n.delete(v) : n.add(v); return n }
const COL_DESC = {
@@ -48,61 +39,42 @@ export default function Leaderboard() {
const [promptFilter, setPromptFilter] = useState('All')
const [analysisTab, setAnalysisTab] = useState('tiers')
const [error, setError] = useState(null)
+ const [runsLoading, setRunsLoading] = useState(true)
+ const [dashboardLoading, setDashboardLoading] = useState(false)
const [expanded, setExpanded] = useState(() => new Set())
const [details, setDetails] = useState({})
const [conv, setConv] = useState(null)
const [turns, setTurns] = useState({})
- // run-from-UI
- const [showRun, setShowRun] = useState(false)
- const [catalog, setCatalog] = useState(null)
- const [famFilter, setFamFilter] = useState('All')
- const [selModels, setSelModels] = useState({})
- const [activePreset, setActivePreset] = useState(null)
- const [profiles, setProfiles] = useState([])
const [prompts, setPrompts] = useState([])
- const [selP, setSelP] = useState(new Set())
- const [runName, setRunName] = useState('')
- const [runStatus, setRunStatus] = useState(null)
- const pollRef = useRef(null)
-
- // guided walkthrough
- const [tour, setTour] = useState(false)
- const [tourStep, setTourStep] = useState(0)
- const boardRef = useRef(board)
- const expandedRef = useRef(expanded)
const detailsRef = useRef(details)
- useEffect(() => { boardRef.current = board }, [board])
- useEffect(() => { expandedRef.current = expanded }, [expanded])
useEffect(() => { detailsRef.current = details }, [details])
- function loadRuns(select) {
- return api('/api/runs').then((r) => {
- setRuns(r); if (select) setRun(select); else if (r.length && !run) setRun(r[0])
- })
+ function loadRuns() {
+ setRunsLoading(true)
+ return api('/api/runs')
+ .then((r) => {
+ setRuns(r)
+ if (r.length) setRun((current) => current || r[0])
+ })
+ .finally(() => setRunsLoading(false))
}
useEffect(() => {
loadRuns().catch((e) => setError(String(e)))
api('/api/meta').then(setMeta).catch(() => {})
- api('/api/grid-options').then((o) => { setPrompts(o.prompts); setSelP(new Set(o.prompts.map((p) => p.name))) }).catch(() => {})
- api('/api/profiles').then(setProfiles).catch(() => {})
- api('/api/models').then((c) => {
- setCatalog(c)
- const def = {}
- c.families.forEach((f) => f.models.forEach((m) => { if (m.in_default) def[m.id] = m }))
- setSelModels(def)
- }).catch(() => {})
- return () => clearTimeout(pollRef.current)
+ api('/api/grid-options').then((o) => setPrompts(o.prompts)).catch(() => {})
}, [])
function loadBoard(rid) {
if (!rid) return
+ setDashboardLoading(true)
const q = `?run_id=${encodeURIComponent(rid)}`
return Promise.all([api('/api/leaderboard' + q), api('/api/tiers' + q), api('/api/outcomes' + q)])
.then(([b, t, o]) => { setBoard(b); setTiers(t); setOutcomes(o); setError(null) })
.catch((e) => setError(String(e)))
+ .finally(() => setDashboardLoading(false))
}
useEffect(() => {
@@ -161,58 +133,6 @@ export default function Leaderboard() {
} catch { /* ignore */ }
}
- // composer helpers
- // manual edits diverge from any preset → clear the active-preset highlight
- function toggleModel(m) {
- setActivePreset(null)
- setSelModels((s) => { const n = { ...s }; if (n[m.id]) delete n[m.id]; else n[m.id] = m; return n })
- }
- function applyProfile(p) {
- const byId = {}
- ;(catalog?.families || []).forEach((f) => f.models.forEach((m) => { byId[m.id] = m }))
- const sel = {}
- p.model_ids.forEach((id) => { if (byId[id]) sel[id] = byId[id] })
- setSelModels(sel)
- setActivePreset(p.name)
- }
- const families = catalog ? catalog.families : []
- const shownModels = famFilter === 'All'
- ? families.flatMap((f) => f.models)
- : (families.find((f) => f.family === famFilter)?.models || [])
- const selCount = Object.keys(selModels).length
-
- function pollStatus() {
- api('/api/run/status').then((s) => {
- setRunStatus(s)
- if (s.running) pollRef.current = setTimeout(pollStatus, 1500)
- else if (s.run_id) { loadRuns(s.run_id); setTimeout(() => loadBoard(s.run_id), 800) }
- }).catch(() => { pollRef.current = setTimeout(pollStatus, 2500) })
- }
- async function startRun() {
- try {
- const r = await post('/api/run', {
- models: Object.values(selModels), prompts: [...selP], run_id: runName.trim() || undefined,
- })
- if (!r.ok) { setRunStatus({ running: false, lines: ['⚠ ' + (r.error || 'failed to start')] }); return }
- setRunStatus({ running: true, run_id: r.run_id, lines: ['starting…'] })
- clearTimeout(pollRef.current); pollRef.current = setTimeout(pollStatus, 1200)
- } catch (e) { setRunStatus({ running: false, lines: ['⚠ ' + String(e)] }) }
- }
- const running = runStatus && runStatus.running
-
- // imperative handle the guided walkthrough drives
- const tourCtl = {
- getBoard: () => boardRef.current,
- isExpanded: (cid) => expandedRef.current.has(cid),
- selectRun: (r) => { if (r && r !== run && runs.includes(r)) setRun(r) },
- setSort: (key, dir = -1) => setSort({ key, dir }),
- setPrompt: (p) => setPromptFilter(p),
- collapseAll: () => { setExpanded(new Set()); setConv(null) },
- openOnly: (cid) => { setExpanded(new Set([cid])); loadDetails(cid) },
- openConv: (cid) => openConversation(cid),
- sessionUrl: (cid) => sessionUrl(cid),
- }
-
if (error) {
return (
@@ -229,24 +149,14 @@ export default function Leaderboard() {
{/* ---------- controls ---------- */}
-
+
-
-
-
- {tourStep > 0 && !tour && (
-
- )}
{meta.datasets_url && (
@@ -255,120 +165,18 @@ export default function Leaderboard() {
)}
- {tour && (
-
{ setTour(false); if (finished) setTourStep(0) }}
- />
- )}
-
- {/* ---------- composer ---------- */}
- {showRun && (
-
-
-
-
Build the arena — {catalog
- ? `${catalog.families.reduce((n, f) => n + f.models.length, 0)} models ${catalog.degraded ? 'from config.yaml' : `live in ${catalog.region}`}`
- : 'loading models…'} · {selCount} selected
- {catalog?.degraded && (
-
- ⚠ live catalog unavailable
-
- )}
-
-
-
-
- {profiles.length > 0 && (
-
-
Presets · one-click selections from config.yaml
-
- {profiles.map((p) => (
-
- ))}
-
-
- )}
-
-
-
Live model catalog{catalog ? ` · ${catalog.region}` : ''} · {selCount} selected
-
-
- {families.map((f) => (
-
- ))}
-
-
- {shownModels.map((m) => {
- const on = !!selModels[m.id]
- return (
-
toggleModel(m)}
- data-tooltip-id="tip" data-tooltip-content={`${m.id} · ${m.price_in ? `$${m.price_in}/$${m.price_out} per 1M` : 'no price set → cost shown as $0'}`}>
-
{on && }
-
-
{m.name}
- {m.size &&
{m.size}}
-
- )
- })}
-
-
-
-
-
-
Prompt strategies
-
- {prompts.map((p) => {
- const pm = promptMeta(p.name, promptDesc)
- return (
-
- )
- })}
-
-
-
-
Run name
-
setRunName(e.target.value.replace(/[^A-Za-z0-9._-]/g, '-'))} style={{ width: '100%', marginBottom: 12 }} />
-
-
-
Graded by LangFuse evaluators — correctness (code) + LLM judge
- {lfBase &&
configure }
-
-
-
-
-
- {selCount} × {selP.size} = {selCount * selP.size} configs × 18 Qs = {selCount * selP.size * 18} agent calls
-
-
-
- {runStatus && (
-
-
- {running && }
- {running ? `running ${runStatus.run_id}…`
- : runStatus.returncode === 0 ? `✓ done — run "${runStatus.run_id}" loaded from Langfuse`
- : runStatus.returncode != null ? `exited (code ${runStatus.returncode})` : ''}
-
-
{(runStatus.lines || []).slice(-14).join('\n')}
-
- )}
+ {dashboardLoading ? (
+
+
+
+ Loading dashboard data
+ Fetching leaderboard, tiers, and outcomes for {run}.
- )}
+ ) : !run && !runsLoading ? (
+
No benchmark runs found.
+ ) : (
+ <>
{/* ---------- KPI strip ---------- */}
@@ -388,7 +196,7 @@ export default function Leaderboard() {
Leaderboard — click a row to drill into per-question LangFuse traces
{promptsInRun.length > 1 && (
-
+
Filter by prompt
{promptsInRun.map((p) => {
@@ -407,7 +215,7 @@ export default function Leaderboard() {
|
{COLS.map(([k, label]) => (
- toggleSort(k)} data-tour-col={k}
+ | toggleSort(k)}
data-tooltip-id="tip" data-tooltip-content={COL_DESC[k]}>
{label}{sort.key === k && {sort.dir < 0 ? '↓' : '↑'}}
|
@@ -420,7 +228,7 @@ export default function Leaderboard() {
const open = expanded.has(r.config_id)
return (
- toggleExpand(r.config_id)}>
+
toggleExpand(r.config_id)}>
| {i + 1} |
@@ -476,6 +284,8 @@ export default function Leaderboard() {
: matchP(o.config_id))} promptDesc={promptDesc} />}
+ >
+ )}
)
@@ -519,18 +329,18 @@ function DrillDown({ config_id, rows, sessionUrl, onConv, conv, turns, loadTurns
const err = rows && rows.error
const list = Array.isArray(rows) ? rows : []
return (
-
+
per-question results — trace ↗ opens the LangFuse trace
{sessionUrl && session ↗}
-
- {loading && Loading per-question results from ClickHouse… }
+ {loading && Loading per-question results from LangFuse… }
{err && (
Couldn't load per-question results: {rows.error}. Is the dashboard API up to date? Restart it (scripts/arena.sh serve).
)}
@@ -610,7 +420,7 @@ function CostScatter({ rows, promptDesc }) {
const seenFam = new Set()
rows.forEach((r) => { const f = famOf(r.model_name); if (!seenFam.has(f.key)) { seenFam.add(f.key); legend.push(f) } })
return (
-
+
Cost × accuracy
bubble size = total spend
diff --git a/workshops/agent_arena/web/src/leaderboard/leaderboard.css b/workshops/agent_arena/web/src/leaderboard/leaderboard.css
index 5c110c8..46fe59f 100644
--- a/workshops/agent_arena/web/src/leaderboard/leaderboard.css
+++ b/workshops/agent_arena/web/src/leaderboard/leaderboard.css
@@ -12,6 +12,23 @@
.run-select { display: inline-flex; align-items: center; gap: 8px; }
.run-select label { font-size: 12px; color: var(--ink-3); font-weight: 600; }
.run-select select { min-width: 200px; font-family: var(--f-mono); font-size: 12.5px; }
+.run-select[aria-busy="true"] select { color: var(--ink-3); }
+
+.dashboard-loading, .dashboard-empty {
+ min-height: 220px; display: flex; align-items: center; justify-content: center;
+ gap: 13px; padding: 28px; border: 1px solid var(--line); border-radius: var(--r-lg);
+ background: var(--bg-2); color: var(--ink-3);
+}
+.dashboard-loading > div { display: flex; flex-direction: column; gap: 4px; }
+.dashboard-loading b { color: var(--ink); font-size: 14px; }
+.dashboard-loading span, .dashboard-empty { font-size: 12.5px; }
+.dashboard-loading code { color: var(--accent); font-family: var(--f-mono); }
+.loading-spinner {
+ width: 16px; height: 16px; flex: none; border: 2px solid var(--line-strong);
+ border-top-color: var(--accent); border-radius: 50%; animation: dashboard-spin .75s linear infinite;
+}
+@keyframes dashboard-spin { to { transform: rotate(360deg); } }
+@media (prefers-reduced-motion: reduce) { .loading-spinner { animation-duration: 1.8s; } }
/* prompt filter chip row */
.prompt-filter { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 4px 0 14px; }
@@ -142,43 +159,6 @@
.cpc-row .label .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cpc-row .val { text-align: right; font-family: var(--f-mono); font-size: 12.5px; font-weight: 700; color: var(--accent); }
-/* ---------- composer (run benchmark) ---------- */
-.composer { background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--r-lg); overflow: hidden; }
-.composer-head { padding: 14px 18px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 10px; }
-.composer-head .t { font-family: var(--f-mono); font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-2); }
-.composer-body { padding: 16px 18px; display: flex; flex-direction: column; gap: 16px; }
-.cmp-grp-label { font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 8px; }
-.preset-row, .family-row { display: flex; gap: 8px; flex-wrap: wrap; }
-.modelgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 8px; max-height: 280px; overflow: auto; padding: 2px; }
-.modelcard { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border-radius: var(--r-sm); border: 1px solid var(--line-2); background: var(--bg-inset); cursor: pointer; transition: all .13s; }
-.modelcard:hover { border-color: var(--line-strong); }
-.modelcard[data-on="true"] { border-color: var(--oc-correct); background: color-mix(in oklab, var(--oc-correct) 12%, var(--bg-inset)); }
-.modelcard .mc-check { width: 16px; height: 16px; border-radius: 4px; border: 1.5px solid var(--line-strong); display: grid; place-items: center; flex: none; }
-.modelcard[data-on="true"] .mc-check { background: var(--oc-correct); border-color: var(--oc-correct); }
-.modelcard .mc-name { font-size: 12.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.modelcard .mc-size { font-family: var(--f-mono); font-size: 9.5px; color: var(--ink-3); margin-left: auto; padding: 1px 6px; border-radius: 5px; background: var(--bg-3); white-space: nowrap; }
-.cmp-foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
-.cmp-foot .estimate { font-family: var(--f-mono); font-size: 12px; color: var(--ink-2); }
-.cmp-foot .estimate b { color: var(--accent); }
-.prompts-col { display: flex; flex-direction: column; gap: 8px; }
-
-/* grading-runs-in-LangFuse indicator (replaces the old LLM-judge checkbox) */
-.lf-grading {
- display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
- font-size: 12.5px; color: var(--ink-2); cursor: help;
- padding: 7px 10px; border-radius: var(--r-sm);
- background: color-mix(in oklab, var(--langfuse) 12%, var(--bg-2));
- border: 1px solid color-mix(in oklab, var(--langfuse) 35%, transparent);
-}
-.lf-grading b { color: var(--ink); }
-.lf-grading a { margin-left: 2px; }
-
-/* live run progress */
-.runlog { background: #05070a; border: 1px solid var(--line); border-radius: var(--r-sm); padding: 10px 12px; font-family: var(--f-mono); font-size: 11.5px; color: #9fe7a8; max-height: 180px; overflow: auto; white-space: pre-wrap; margin: 0; }
-.run-status-line { font-family: var(--f-mono); font-size: 12px; color: var(--ink-3); margin-bottom: 6px; display: inline-flex; align-items: center; gap: 8px; }
-.run-status-line.running .pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: blink 1s steps(1) infinite; }
-.run-status-line.done { color: var(--oc-correct); }
-
/* layout grids */
.lb-hero { display: grid; grid-template-columns: 1.55fr 1fr; gap: var(--gap); align-items: stretch; }
.side-card { background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 14px 15px; }
@@ -193,53 +173,3 @@
@media (max-width: 1080px) {
.lb-hero { grid-template-columns: 1fr; }
}
-
-/* ---------- guided walkthrough ---------- */
-.lb-tourbtn {
- display: inline-flex; align-items: center; gap: 7px; height: 36px; padding: 0 14px;
- border-radius: var(--r-sm); cursor: pointer; font-size: 13.5px; font-weight: 700;
- color: var(--accent); background: color-mix(in oklab, var(--accent) 12%, var(--bg-2));
- border: 1px solid color-mix(in oklab, var(--accent) 45%, transparent);
- transition: all .14s;
-}
-.lb-tourbtn:hover { background: color-mix(in oklab, var(--accent) 22%, var(--bg-2)); box-shadow: 0 0 24px -8px var(--accent-glow); }
-
-.tour-scrim { position: fixed; inset: 0; z-index: 9000; background: transparent; cursor: default; }
-.tour-hole {
- position: fixed; z-index: 9001; border-radius: 12px; pointer-events: none;
- box-shadow: 0 0 0 9999px rgba(5,7,10,.74), 0 0 0 2px var(--accent), 0 0 30px -2px var(--accent-glow);
- transition: left .34s cubic-bezier(.4,0,.2,1), top .34s cubic-bezier(.4,0,.2,1), width .34s cubic-bezier(.4,0,.2,1), height .34s cubic-bezier(.4,0,.2,1);
- animation: tourPulse 2.1s ease-in-out infinite;
-}
-@keyframes tourPulse {
- 0%,100% { box-shadow: 0 0 0 9999px rgba(5,7,10,.74), 0 0 0 2px var(--accent), 0 0 26px -4px var(--accent-glow); }
- 50% { box-shadow: 0 0 0 9999px rgba(5,7,10,.74), 0 0 0 2px var(--accent), 0 0 44px 2px var(--accent-glow); }
-}
-.tour-card {
- position: fixed; z-index: 9002; width: 360px; max-width: 92vw;
- background: linear-gradient(180deg, var(--bg-2), color-mix(in oklab, var(--bg-2) 88%, #000));
- border: 1px solid var(--line-2); border-radius: var(--r-lg); box-shadow: var(--sh-3);
- padding: 16px 18px 14px; transition: left .3s, top .3s;
-}
-.tour-x { position: absolute; top: 10px; right: 10px; display: grid; place-items: center; width: 26px; height: 26px; border-radius: 7px; border: 1px solid var(--line-2); background: var(--bg-3); color: var(--ink-3); cursor: pointer; }
-.tour-x:hover { color: var(--ink); }
-.tour-kicker { font-family: var(--f-mono); font-size: 10.5px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; color: var(--accent); }
-.tour-title { font-family: var(--f-display); font-size: 19px; margin: 5px 0 8px; padding-right: 26px; }
-.tour-body { font-size: 13.5px; line-height: 1.55; color: var(--ink-2); }
-.tour-body b { color: var(--ink); }
-.tour-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; }
-.tour-dots { display: inline-flex; gap: 5px; }
-.tour-dots span { width: 7px; height: 7px; border-radius: 50%; background: var(--line-2); transition: all .2s; }
-.tour-dots span.done { background: color-mix(in oklab, var(--accent) 55%, transparent); }
-.tour-dots span.on { background: var(--accent); transform: scale(1.25); box-shadow: 0 0 10px -1px var(--accent-glow); }
-.tour-link {
- display: inline-flex; align-items: center; justify-content: center; gap: 7px;
- width: 100%; margin-top: 12px; padding: 9px 12px; border-radius: var(--r-sm);
- font-size: 12.5px; font-weight: 700; color: var(--accent);
- background: color-mix(in oklab, var(--accent) 12%, var(--bg-2));
- border: 1px solid color-mix(in oklab, var(--accent) 45%, transparent);
-}
-.tour-link:hover { background: color-mix(in oklab, var(--accent) 20%, var(--bg-2)); text-decoration: none; box-shadow: 0 0 22px -8px var(--accent-glow); }
-.tour-nav { display: inline-flex; align-items: center; gap: 8px; }
-.tour-count { font-size: 11px; color: var(--ink-3); }
-.tour-nav .btn.sm { height: 30px; }
diff --git a/workshops/agent_arena/web/src/main.jsx b/workshops/agent_arena/web/src/main.jsx
index 3af6864..68acfee 100644
--- a/workshops/agent_arena/web/src/main.jsx
+++ b/workshops/agent_arena/web/src/main.jsx
@@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './theme.css'
import './shell.css'
-import './styles.css'
createRoot(document.getElementById('root')).render(
diff --git a/workshops/agent_arena/web/src/meta.js b/workshops/agent_arena/web/src/meta.js
index 37c559c..cd8f4ab 100644
--- a/workshops/agent_arena/web/src/meta.js
+++ b/workshops/agent_arena/web/src/meta.js
@@ -16,9 +16,6 @@ export const FAMILIES = {
other: { key: 'other', label: 'Other', vendor: '', color: 'var(--ink-3)' },
}
-// Ordered list of fighters for the countdown stage.
-export const FIGHTER_FAMILIES = ['claude', 'openai', 'google', 'xai', 'deepseek', 'qwen', 'kimi', 'meta', 'zai']
-
export function famKeyOf(name = '') {
const s = String(name).toLowerCase()
if (s.includes('claude')) return 'claude'
diff --git a/workshops/agent_arena/web/src/shell.css b/workshops/agent_arena/web/src/shell.css
index 57beb46..7e565f9 100644
--- a/workshops/agent_arena/web/src/shell.css
+++ b/workshops/agent_arena/web/src/shell.css
@@ -30,8 +30,3 @@
.app-body { flex: 1; min-height: 0; position: relative; overflow: auto; }
.app-body .view-fill { height: 100%; }
-
-/* mini countdown indicator that rides in the nav while on another tab */
-.cd-mini { display: inline-flex; align-items: center; gap: 7px; font-family: var(--f-mono); font-size: 12px; color: var(--ink-2); padding: 4px 9px; border-radius: 999px; border: 1px solid var(--line-2); background: var(--bg-inset); }
-.cd-mini .d { color: var(--accent); font-weight: 700; }
-.cd-mini.run .d { animation: blink 1s steps(1) infinite; }
diff --git a/workshops/agent_arena/web/src/styles.css b/workshops/agent_arena/web/src/styles.css
deleted file mode 100644
index 138bac3..0000000
--- a/workshops/agent_arena/web/src/styles.css
+++ /dev/null
@@ -1,63 +0,0 @@
-/* Architecture diagram styles (React Flow). Shell/leaderboard/countdown
- live in theme.css + shell.css + leaderboard.css + countdown.css. */
-
-.flow-fill { width: 100%; height: 100%; }
-
-/* ---- Cards ---- */
-.card-node {
- display: flex; gap: 10px; align-items: center;
- padding: 10px 12px; border-radius: var(--r-md);
- background: var(--bg-2);
- border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line-2));
- box-shadow: var(--sh-2), inset 0 0 0 1px rgba(255,255,255,.02);
- border-left: 4px solid var(--accent);
-}
-.card-icon { font-size: 20px; line-height: 1; filter: saturate(1.2); }
-.card-title { font-size: 13.5px; font-weight: 650; color: var(--ink); font-family: var(--f-display); }
-.card-sub { font-size: 11px; color: var(--ink-3); margin-top: 2px; }
-.card-handle { width: 6px; height: 6px; background: var(--line-2); border: none; opacity: 0; }
-
-/* ---- Edges ---- */
-.flow-dash { animation: dash 1s linear infinite; opacity: .8; }
-@keyframes dash { to { stroke-dashoffset: -32; } }
-.flow-packet { filter: drop-shadow(0 0 4px currentColor); }
-
-.edge-label {
- position: absolute; pointer-events: auto; cursor: help;
- font-size: 9.5px; font-weight: 600; color: var(--ink-2); letter-spacing: .01em;
- font-family: var(--f-mono);
- background: rgba(8, 9, 12, 0.85); padding: 0 5px; border-radius: 6px;
- border: 1px solid color-mix(in srgb, var(--edge-color) 40%, transparent);
- white-space: nowrap; line-height: 1.5;
-}
-.edge-label:hover { color: #fff; z-index: 30; border-color: var(--edge-color); }
-.edge-tip {
- display: none; position: absolute; bottom: 150%; left: 50%;
- transform: translateX(-50%); white-space: nowrap;
- background: #05070a; color: var(--ink); font-weight: 500; font-size: 11px;
- padding: 4px 9px; border-radius: 8px; z-index: 40;
- border: 1px solid var(--edge-color);
- box-shadow: var(--sh-2);
-}
-.edge-tip::after {
- content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
- border: 5px solid transparent; border-top-color: var(--edge-color);
-}
-.edge-label:hover .edge-tip { display: block; }
-
-/* ---- Panels / legend ---- */
-.legend-panel {
- background: color-mix(in oklab, var(--bg-2) 92%, transparent); border: 1px solid var(--line);
- border-radius: var(--r-md); padding: 10px 12px; font-size: 11.5px;
- backdrop-filter: blur(8px);
-}
-.legend-row { display: flex; align-items: center; gap: 8px; margin: 3px 0; color: var(--ink-2); }
-.legend-swatch { width: 16px; height: 4px; border-radius: 3px; display: inline-block; }
-.legend-head { font-family: var(--f-mono); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-3); margin-bottom: 3px; }
-.legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
-
-.react-flow__minimap { border-radius: 10px; border: 1px solid var(--line); }
-.react-flow__controls button {
- background: var(--bg-2); border-bottom: 1px solid var(--line); color: var(--ink); fill: var(--ink);
-}
-.react-flow__controls button:hover { background: var(--bg-3); }
diff --git a/workshops/agent_arena/web/src/ui.jsx b/workshops/agent_arena/web/src/ui.jsx
index 2255657..74b9093 100644
--- a/workshops/agent_arena/web/src/ui.jsx
+++ b/workshops/agent_arena/web/src/ui.jsx
@@ -31,14 +31,10 @@ export function BrandLock({ compact }) {
export function Icon({ name, size = 18, color = 'currentColor', strokeWidth = 1.7 }) {
const p = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round' }
switch (name) {
- case 'clock': return
- case 'flow': return
case 'trophy': return
case 'ext': return
case 'chev': return
- case 'play': return
case 'bolt': return
- case 'x': return
case 'dollar': return
case 'scatter': return
case 'grid': return
|