Spaces:
Sleeping
web UI overhaul + fix #32 (plan phase save) (#43)
Browse filesWeb UI (Chainlit):
- Rebrand to Codabench: blue theme + Codabench wordmark/globe logos.
- One settings gear in the header (theme · Readme · logout); hide the native
theme/avatar/Readme controls.
- Phase bar: instant hover tooltips per phase, and the next phase pill becomes
clickable to advance once its predecessor's artifact exists.
- Always-visible cost/context widget; fix the context % (it counted only
uncached input tokens, so it read ~0% — now includes cache read/creation).
- Per-turn tool-step log collapses into a hide/unhide <details> when the
answer lands.
- Tutorial-style Readme; greeting starter examples + a downloadable example
bundle for the validate path.
- Serve the SPA shell + custom JS/CSS with no-store so reloads stop running a
stale chat.js/login.css.
- Banner no longer persists on the landing; workspace panel starts below the
header so it no longer clips the phase bar.
Core (fix #32 — "plan phase finished without saving specs/implementation_plan.md"):
- The plan skill told the agent to save with
autocodabench_snapshot_spec(name="implementation_plan", ...) but the tool
parameter is `filename`, written verbatim with no `.md`, so the file landed
at specs/implementation_plan and the pipeline reported it as never saved.
- Correct the skill to filename="implementation_plan.md", add a tolerant
_resolve_plan_file() that promotes an extension-less save, and add regression
tests.
- src/autocodabench/agent/pipeline.py +25 -5
- src/autocodabench/skills/plan/SKILL.md +6 -3
- tests/test_pipeline_phases.py +59 -0
- web/.chainlit/config.toml +6 -3
- web/app.py +34 -0
- web/chainlit.md +91 -37
- web/public/avatars/autocodabench.png +0 -0
- web/public/chat.js +367 -24
- web/public/codabench-logo.png +0 -0
- web/public/examples/example-bundle-survival.zip +0 -0
- web/public/login.css +401 -133
- web/public/logo_dark.png +0 -0
- web/public/logo_light.png +0 -0
- web/session_manager.py +74 -27
- web/streaming.py +54 -18
|
@@ -136,6 +136,26 @@ def _find_bundle(run_dir: Path) -> tuple[Path | None, Path | None]:
|
|
| 136 |
return None, None
|
| 137 |
|
| 138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
def _update_run_slug(run_dir: Path, slug: str) -> None:
|
| 140 |
"""Overwrite the slug field in an existing run's meta.json.
|
| 141 |
|
|
@@ -272,11 +292,11 @@ async def create_async(
|
|
| 272 |
emit({"kind": "phase_done", "phase": "plan", "ok": plan_result.ok,
|
| 273 |
"num_turns": plan_result.num_turns,
|
| 274 |
"cost_usd": plan_result.total_cost_usd})
|
| 275 |
-
plan_path = p1.path
|
| 276 |
record_session_phase(run_dir, "phase1_plan", {
|
| 277 |
-
"dir": str(p1.path), "ok": bool(plan_result.ok and plan_path
|
| 278 |
"cost_usd": plan_result.total_cost_usd, "num_turns": plan_result.num_turns})
|
| 279 |
-
if not plan_result.ok or
|
| 280 |
return CreateResult(
|
| 281 |
ok=False, run_dir=run_dir, plan_dir=p1.path,
|
| 282 |
plan=plan_result,
|
|
@@ -442,8 +462,8 @@ async def plan_async(
|
|
| 442 |
allow_web_tools=research_resolved.web_search,
|
| 443 |
))
|
| 444 |
|
| 445 |
-
plan_path = run_dir
|
| 446 |
-
if not plan_result.ok or
|
| 447 |
return PlanResult(
|
| 448 |
ok=False, run_dir=run_dir,
|
| 449 |
plan=plan_result,
|
|
|
|
| 136 |
return None, None
|
| 137 |
|
| 138 |
|
| 139 |
+
def _resolve_plan_file(run_path: Path) -> Path | None:
|
| 140 |
+
"""Return the saved implementation plan, or None if the agent never wrote it.
|
| 141 |
+
|
| 142 |
+
Defense-in-depth for the class of bug behind #32: the plan agent is told to
|
| 143 |
+
save `specs/implementation_plan.md`, but `snapshot_spec` writes the filename
|
| 144 |
+
verbatim, so an agent that drops the extension lands at
|
| 145 |
+
`specs/implementation_plan`. Phase 2 reads `implementation_plan.md`, so if we
|
| 146 |
+
find only the extension-less file we promote it to the canonical name.
|
| 147 |
+
"""
|
| 148 |
+
specs = run_path / "specs"
|
| 149 |
+
canonical = specs / "implementation_plan.md"
|
| 150 |
+
if canonical.is_file():
|
| 151 |
+
return canonical
|
| 152 |
+
extensionless = specs / "implementation_plan"
|
| 153 |
+
if extensionless.is_file():
|
| 154 |
+
extensionless.rename(canonical)
|
| 155 |
+
return canonical
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
|
| 159 |
def _update_run_slug(run_dir: Path, slug: str) -> None:
|
| 160 |
"""Overwrite the slug field in an existing run's meta.json.
|
| 161 |
|
|
|
|
| 292 |
emit({"kind": "phase_done", "phase": "plan", "ok": plan_result.ok,
|
| 293 |
"num_turns": plan_result.num_turns,
|
| 294 |
"cost_usd": plan_result.total_cost_usd})
|
| 295 |
+
plan_path = _resolve_plan_file(p1.path)
|
| 296 |
record_session_phase(run_dir, "phase1_plan", {
|
| 297 |
+
"dir": str(p1.path), "ok": bool(plan_result.ok and plan_path is not None),
|
| 298 |
"cost_usd": plan_result.total_cost_usd, "num_turns": plan_result.num_turns})
|
| 299 |
+
if not plan_result.ok or plan_path is None:
|
| 300 |
return CreateResult(
|
| 301 |
ok=False, run_dir=run_dir, plan_dir=p1.path,
|
| 302 |
plan=plan_result,
|
|
|
|
| 462 |
allow_web_tools=research_resolved.web_search,
|
| 463 |
))
|
| 464 |
|
| 465 |
+
plan_path = _resolve_plan_file(run_dir)
|
| 466 |
+
if not plan_result.ok or plan_path is None:
|
| 467 |
return PlanResult(
|
| 468 |
ok=False, run_dir=run_dir,
|
| 469 |
plan=plan_result,
|
|
@@ -26,7 +26,9 @@ which is exactly the kind of cost-burn we're trying to avoid.
|
|
| 26 |
2. **No code, no notebook.** Don't write Python in chat code-fences,
|
| 27 |
don't call any `nb_*` tool. Phase 2 writes code.
|
| 28 |
3. **One artifact: `<run>/specs/implementation_plan.md`.** Save via
|
| 29 |
-
`autocodabench_snapshot_spec(
|
|
|
|
|
|
|
| 30 |
4. **Be specific, but version-robust.** Every section must name
|
| 31 |
concrete things Phase 2 can implement without asking. See the §2
|
| 32 |
template — fields like "primary metric" should be
|
|
@@ -213,7 +215,7 @@ Once drafted, save:
|
|
| 213 |
|
| 214 |
```
|
| 215 |
autocodabench_snapshot_spec(
|
| 216 |
-
|
| 217 |
body=<the full markdown>,
|
| 218 |
)
|
| 219 |
|
|
@@ -336,7 +338,8 @@ From `autocodabench`:
|
|
| 336 |
- `autocodabench_open_run(slug?)` — once, first tool call.
|
| 337 |
- `autocodabench_current_run()` — sanity check the run dir.
|
| 338 |
- `autocodabench_log_event(kind, payload?)`.
|
| 339 |
-
- `autocodabench_snapshot_spec(
|
|
|
|
| 340 |
|
| 341 |
**Research tools — ground the design in the existing literature and in
|
| 342 |
hosting practice, not in your prior alone.** Two *structured* sources
|
|
|
|
| 26 |
2. **No code, no notebook.** Don't write Python in chat code-fences,
|
| 27 |
don't call any `nb_*` tool. Phase 2 writes code.
|
| 28 |
3. **One artifact: `<run>/specs/implementation_plan.md`.** Save via
|
| 29 |
+
`autocodabench_snapshot_spec(filename="implementation_plan.md", body=<md>)`.
|
| 30 |
+
The argument is `filename` (NOT `name`) and it is written verbatim — it MUST
|
| 31 |
+
end in `.md`, or Phase 2 cannot find the plan.
|
| 32 |
4. **Be specific, but version-robust.** Every section must name
|
| 33 |
concrete things Phase 2 can implement without asking. See the §2
|
| 34 |
template — fields like "primary metric" should be
|
|
|
|
| 215 |
|
| 216 |
```
|
| 217 |
autocodabench_snapshot_spec(
|
| 218 |
+
filename="implementation_plan.md",
|
| 219 |
body=<the full markdown>,
|
| 220 |
)
|
| 221 |
|
|
|
|
| 338 |
- `autocodabench_open_run(slug?)` — once, first tool call.
|
| 339 |
- `autocodabench_current_run()` — sanity check the run dir.
|
| 340 |
- `autocodabench_log_event(kind, payload?)`.
|
| 341 |
+
- `autocodabench_snapshot_spec(filename, body)` — save / revise the plan
|
| 342 |
+
(use `filename="implementation_plan.md"`).
|
| 343 |
|
| 344 |
**Research tools — ground the design in the existing literature and in
|
| 345 |
hosting practice, not in your prior alone.** Two *structured* sources
|
|
@@ -21,3 +21,62 @@ def test_bundle_async_rejects_both_sources(tmp_path):
|
|
| 21 |
plan.write_text("# plan", encoding="utf-8")
|
| 22 |
with pytest.raises(ValueError, match="exactly one"):
|
| 23 |
asyncio.run(bundle_async(run_dir=tmp_path, plan_path=plan))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
plan.write_text("# plan", encoding="utf-8")
|
| 22 |
with pytest.raises(ValueError, match="exactly one"):
|
| 23 |
asyncio.run(bundle_async(run_dir=tmp_path, plan_path=plan))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_plan_prompt_save_call_matches_tool_and_pipeline():
|
| 27 |
+
"""Regression for #32: 'plan phase finished without saving
|
| 28 |
+
specs/implementation_plan.md'.
|
| 29 |
+
|
| 30 |
+
The plan skill told the agent to save with
|
| 31 |
+
`autocodabench_snapshot_spec(name="implementation_plan", ...)` — but the tool
|
| 32 |
+
parameter is `filename` (no `name` alias) and `snapshot_spec` writes the
|
| 33 |
+
name verbatim, so the file landed at `specs/implementation_plan` (no `.md`)
|
| 34 |
+
or the call errored. The pipeline then looked for
|
| 35 |
+
`specs/implementation_plan.md` and reported the plan as never saved.
|
| 36 |
+
|
| 37 |
+
The system prompt the plan agent actually sees must therefore (a) use the
|
| 38 |
+
real `filename` parameter and (b) include the `.md` the pipeline checks for.
|
| 39 |
+
"""
|
| 40 |
+
import inspect
|
| 41 |
+
|
| 42 |
+
from autocodabench.run_log import snapshot_spec
|
| 43 |
+
from autocodabench.agent.prompts import plan_system_prompt
|
| 44 |
+
|
| 45 |
+
# The save tool's first parameter is `filename` (no `name`).
|
| 46 |
+
assert list(inspect.signature(snapshot_spec).parameters)[0] == "filename"
|
| 47 |
+
|
| 48 |
+
prompt = plan_system_prompt()
|
| 49 |
+
# Must not instruct a kwarg/value that yields the wrong path.
|
| 50 |
+
assert 'name="implementation_plan"' not in prompt, (
|
| 51 |
+
"plan prompt still tells the agent to save with `name=` / without `.md`"
|
| 52 |
+
)
|
| 53 |
+
# Must instruct the exact save the pipeline can find.
|
| 54 |
+
assert 'filename="implementation_plan.md"' in prompt
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def test_resolve_plan_file_promotes_extensionless(tmp_path):
|
| 58 |
+
"""Defense-in-depth: a plan saved as `implementation_plan` (no `.md`) is
|
| 59 |
+
promoted to the canonical `implementation_plan.md` that Phase 2 reads."""
|
| 60 |
+
from autocodabench.agent.pipeline import _resolve_plan_file
|
| 61 |
+
|
| 62 |
+
specs = tmp_path / "specs"
|
| 63 |
+
specs.mkdir()
|
| 64 |
+
assert _resolve_plan_file(tmp_path) is None # nothing saved
|
| 65 |
+
|
| 66 |
+
(specs / "implementation_plan").write_text("# plan", encoding="utf-8")
|
| 67 |
+
resolved = _resolve_plan_file(tmp_path)
|
| 68 |
+
assert resolved == specs / "implementation_plan.md"
|
| 69 |
+
assert resolved.is_file()
|
| 70 |
+
assert not (specs / "implementation_plan").exists() # renamed, not copied
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_resolve_plan_file_prefers_canonical(tmp_path):
|
| 74 |
+
from autocodabench.agent.pipeline import _resolve_plan_file
|
| 75 |
+
|
| 76 |
+
specs = tmp_path / "specs"
|
| 77 |
+
specs.mkdir()
|
| 78 |
+
(specs / "implementation_plan.md").write_text("# canonical", encoding="utf-8")
|
| 79 |
+
(specs / "implementation_plan").write_text("# stale", encoding="utf-8")
|
| 80 |
+
resolved = _resolve_plan_file(tmp_path)
|
| 81 |
+
assert resolved == specs / "implementation_plan.md"
|
| 82 |
+
assert resolved.read_text(encoding="utf-8") == "# canonical" # didn't clobber
|
|
@@ -132,14 +132,17 @@ cot = "tool_call"
|
|
| 132 |
|
| 133 |
# Specify a CSS file that can be used to customize the user interface.
|
| 134 |
# The CSS file can be served from the public directory or via an external link.
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
# Specify additional attributes for a custom CSS file
|
| 138 |
# custom_css_attributes = "media=\"print\""
|
| 139 |
|
| 140 |
# Specify a JavaScript file that can be used to customize the user interface.
|
| 141 |
# The JavaScript file can be served from the public directory.
|
| 142 |
-
custom_js = "/public/chat.js"
|
| 143 |
|
| 144 |
# The style of alert boxes. Can be "classic" or "modern".
|
| 145 |
alert_style = "classic"
|
|
@@ -161,7 +164,7 @@ alert_style = "classic"
|
|
| 161 |
# custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png"
|
| 162 |
|
| 163 |
# Load assistant logo directly from URL.
|
| 164 |
-
logo_file_url = ""
|
| 165 |
|
| 166 |
# Load assistant avatar image directly from URL.
|
| 167 |
default_avatar_file_url = ""
|
|
|
|
| 132 |
|
| 133 |
# Specify a CSS file that can be used to customize the user interface.
|
| 134 |
# The CSS file can be served from the public directory or via an external link.
|
| 135 |
+
# NOTE: the ?v= query string is a cache-buster. Chainlit serves no
|
| 136 |
+
# Cache-Control header on /public assets, so browsers can hold a stale
|
| 137 |
+
# copy. Bump the version whenever login.css / chat.js change materially.
|
| 138 |
+
custom_css = "/public/login.css?v=15"
|
| 139 |
|
| 140 |
# Specify additional attributes for a custom CSS file
|
| 141 |
# custom_css_attributes = "media=\"print\""
|
| 142 |
|
| 143 |
# Specify a JavaScript file that can be used to customize the user interface.
|
| 144 |
# The JavaScript file can be served from the public directory.
|
| 145 |
+
custom_js = "/public/chat.js?v=15"
|
| 146 |
|
| 147 |
# The style of alert boxes. Can be "classic" or "modern".
|
| 148 |
alert_style = "classic"
|
|
|
|
| 164 |
# custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png"
|
| 165 |
|
| 166 |
# Load assistant logo directly from URL.
|
| 167 |
+
logo_file_url = "/public/codabench-logo.png"
|
| 168 |
|
| 169 |
# Load assistant avatar image directly from URL.
|
| 170 |
default_avatar_file_url = ""
|
|
@@ -62,6 +62,40 @@ log = logging.getLogger("autocodabench.web")
|
|
| 62 |
register_upload_route()
|
| 63 |
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
# ---------------------------------------------------------------------------
|
| 66 |
# Auth
|
| 67 |
# ---------------------------------------------------------------------------
|
|
|
|
| 62 |
register_upload_route()
|
| 63 |
|
| 64 |
|
| 65 |
+
def _install_no_cache_for_custom_assets() -> None:
|
| 66 |
+
"""Force browsers to always re-fetch the SPA shell + our custom JS/CSS.
|
| 67 |
+
|
| 68 |
+
Chainlit serves `/` and `/public/*` with NO Cache-Control header, so
|
| 69 |
+
browsers heuristically cache `chat.js` / `login.css` and keep running a
|
| 70 |
+
stale UI across reloads (the recurring "X is gone after refresh"). Marking
|
| 71 |
+
just those responses `no-store` makes every reload pick up the live files.
|
| 72 |
+
Added at import time, before uvicorn starts serving.
|
| 73 |
+
"""
|
| 74 |
+
try:
|
| 75 |
+
from chainlit.server import app as cl_app
|
| 76 |
+
except Exception as e: # pragma: no cover
|
| 77 |
+
log.warning("Chainlit FastAPI app unavailable; no-cache not installed: %s", e)
|
| 78 |
+
return
|
| 79 |
+
if getattr(cl_app, "_ac_no_cache_installed", False):
|
| 80 |
+
return
|
| 81 |
+
cl_app._ac_no_cache_installed = True # type: ignore[attr-defined]
|
| 82 |
+
|
| 83 |
+
_NO_CACHE_PATHS = ("/public/chat.js", "/public/login.css")
|
| 84 |
+
|
| 85 |
+
@cl_app.middleware("http")
|
| 86 |
+
async def _ac_no_cache(request, call_next): # type: ignore[no-untyped-def]
|
| 87 |
+
response = await call_next(request)
|
| 88 |
+
path = request.url.path
|
| 89 |
+
if path == "/" or path in _NO_CACHE_PATHS:
|
| 90 |
+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
| 91 |
+
response.headers["Pragma"] = "no-cache"
|
| 92 |
+
response.headers["Expires"] = "0"
|
| 93 |
+
return response
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
_install_no_cache_for_custom_assets()
|
| 97 |
+
|
| 98 |
+
|
| 99 |
# ---------------------------------------------------------------------------
|
| 100 |
# Auth
|
| 101 |
# ---------------------------------------------------------------------------
|
|
@@ -1,56 +1,110 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
-
##
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
---
|
| 17 |
|
| 18 |
-
##
|
| 19 |
-
|
| 20 |
-
**1. 📝 Plan.** Chat with the agent until it converges on a one-page `implementation_plan.md` covering the 7 design sections (task, data, metric, baseline, rules, ethics, schedule). Review it in the **workspace panel** on the right. When ready, click **▶ Proceed to Phase 2**.
|
| 21 |
|
| 22 |
-
|
| 23 |
|
| 24 |
-
**
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
---
|
| 27 |
|
| 28 |
-
##
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
---
|
| 33 |
|
| 34 |
-
##
|
| 35 |
-
|
| 36 |
-
A **✅ PASS / ❌ FAIL** verdict in chat plus two colorful tables, and a downloadable `validation_report.md`:
|
| 37 |
-
|
| 38 |
-
- **Table A — design scorecard** *(create-path only)*: each of the 7 design sections marked ✅ / ⚠️ / ❌ against best practice.
|
| 39 |
-
- **Table B — checks**: every check with ✅ pass / ❌ fail / ⚠️ finding / 📋 attestation / • skipped.
|
| 40 |
-
- **Gate failures (❌)** must be fixed before upload (e.g. missing `competition.yaml` keys, broken file refs, a baseline that crashes in Docker).
|
| 41 |
-
- **Findings (⚠️)** are advisory design risks (with citations) — they don't block upload.
|
| 42 |
-
- **Attestations (📋)** are criteria only a human can certify.
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
Everything
|
|
|
|
| 47 |
|
| 48 |
---
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
- Logs are uploaded to a private HF Dataset (`ktgiahieu/autocodabench-runs`).
|
| 55 |
-
- Each session has a hard Anthropic budget cap (default **$5.00**, via `MAX_USD_PER_SESSION`).
|
| 56 |
-
- HF Spaces is CPU-only, ≤16 GB RAM; if Docker isn't available, Phase 2 skips the runtime check and Phase 3's Docker execution checks report as *skipped* rather than failing.
|
|
|
|
| 1 |
+
<div align="center" style="margin:6px 0 2px">
|
| 2 |
+
<img src="/public/codabench-logo.png" alt="Codabench" style="height:40px" />
|
| 3 |
+
<div style="color:hsl(var(--muted-foreground));font-size:14px;margin-top:6px">
|
| 4 |
+
Design & validate Codabench competitions — just by chatting.
|
| 5 |
+
</div>
|
| 6 |
+
</div>
|
| 7 |
|
| 8 |
---
|
| 9 |
|
| 10 |
+
### 🧭 The three phases
|
| 11 |
+
|
| 12 |
+
<div style="display:flex;gap:10px;flex-wrap:wrap;margin:8px 0 4px">
|
| 13 |
+
<div style="flex:1;min-width:150px;padding:14px;border:1px solid hsl(var(--border));border-radius:12px;background:hsl(var(--accent))">
|
| 14 |
+
<div style="font-size:24px;line-height:1">📝</div>
|
| 15 |
+
<div style="font-weight:700;margin-top:6px">1 · Plan</div>
|
| 16 |
+
<div style="font-size:12.5px;color:hsl(var(--muted-foreground));margin-top:3px">Chat an idea into a design plan.</div>
|
| 17 |
+
</div>
|
| 18 |
+
<div style="flex:1;min-width:150px;padding:14px;border:1px solid hsl(var(--border));border-radius:12px;background:hsl(var(--accent))">
|
| 19 |
+
<div style="font-size:24px;line-height:1">📦</div>
|
| 20 |
+
<div style="font-weight:700;margin-top:6px">2 · Build</div>
|
| 21 |
+
<div style="font-size:12.5px;color:hsl(var(--muted-foreground));margin-top:3px">Agent writes the bundle & runs it.</div>
|
| 22 |
+
</div>
|
| 23 |
+
<div style="flex:1;min-width:150px;padding:14px;border:1px solid hsl(var(--border));border-radius:12px;background:hsl(var(--accent))">
|
| 24 |
+
<div style="font-size:24px;line-height:1">✅</div>
|
| 25 |
+
<div style="font-weight:700;margin-top:6px">3 · Validate</div>
|
| 26 |
+
<div style="font-size:12.5px;color:hsl(var(--muted-foreground));margin-top:3px">Checks + report: ready to launch?</div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
The **phase bar** (top-right) shows where you are — `●` active · `✓` done.
|
| 31 |
+
Hover any phase for a one-line reminder. You move forward with the **▶ Proceed**
|
| 32 |
+
buttons in the chat.
|
| 33 |
|
| 34 |
---
|
| 35 |
|
| 36 |
+
### 🚀 Two ways to start
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
Every new chat opens with a choice (switch later via **New Chat**, top-left):
|
| 39 |
|
| 40 |
+
- **🛠 Create from scratch** — you have an *idea*. Go through all three phases below.
|
| 41 |
+
- **✅ Validate a bundle** — you already have a `.zip`. Jump straight to **Phase 3**;
|
| 42 |
+
the composer locks to *attach-only* (drop your bundle and send). No bundle? That
|
| 43 |
+
screen has a **⬇ Download example bundle** button to try.
|
| 44 |
|
| 45 |
---
|
| 46 |
|
| 47 |
+
## 📝 Phase 1 · Plan
|
| 48 |
+
|
| 49 |
+
*Design the competition together — the agent acts as a scientific collaborator.*
|
| 50 |
+
|
| 51 |
+
- **You provide:** a one-sentence idea — e.g. *“a fair chest-X-ray pneumonia
|
| 52 |
+
challenge, scored by balanced accuracy.”* You can also **drop a PDF or markdown
|
| 53 |
+
proposal** and it'll seed the plan.
|
| 54 |
+
- **What happens:** the agent researches related work (OpenAlex / Kaggle), then
|
| 55 |
+
works through the **7 design sections** with you — *task, data, metric, baseline,
|
| 56 |
+
rules, ethics, schedule* — asking questions until the design is coherent.
|
| 57 |
+
- **You get:** a one-page **`implementation_plan.md`** in the **workspace panel**
|
| 58 |
+
on the right. Read it, push back, refine.
|
| 59 |
+
- **Next:** when it looks right, click **▶ Proceed to Phase 2**.
|
| 60 |
+
|
| 61 |
+
## 📦 Phase 2 · Competition Creation (Build)
|
| 62 |
+
|
| 63 |
+
*A fresh agent turns the approved plan into a real Codabench bundle.*
|
| 64 |
+
|
| 65 |
+
- **Input:** **only** the locked `implementation_plan.md` — this agent never sees
|
| 66 |
+
your chat, which keeps the build honest (no leaked answers).
|
| 67 |
+
- **What happens:** it writes the full bundle — `competition.yaml`, the
|
| 68 |
+
`scoring_program/`, a **baseline solution**, the ingestion program, participant
|
| 69 |
+
**pages**, and a **starting kit** — then **builds and runs it in Docker** (using
|
| 70 |
+
the bundle's `docker_image`) to prove the baseline actually produces a score.
|
| 71 |
+
Expect **~5–10 min** (longer the first time, while the image downloads).
|
| 72 |
+
- **You get:** a **`bundle.zip`** to download, plus an **⬆️ Upload to Codabench**
|
| 73 |
+
button.
|
| 74 |
+
- **Next:** click **▶ Proceed to Phase 3**.
|
| 75 |
+
|
| 76 |
+
## ✅ Phase 3 · Validation
|
| 77 |
+
|
| 78 |
+
*The pre-launch safety check — would this competition run cleanly?*
|
| 79 |
+
|
| 80 |
+
- **Input:** the bundle — automatically from Phase 2, or (Validate path) the
|
| 81 |
+
`.zip` you **attach**.
|
| 82 |
+
- **What happens:** runs the full **check framework** — schema and file-reference
|
| 83 |
+
checks, plus a real **Docker execution of the baseline** — and, on the create
|
| 84 |
+
path, a **design scorecard** grading your plan against best practice. You can
|
| 85 |
+
then run an optional **✨ LLM-judged** pass that flags participant pages
|
| 86 |
+
contradicting `competition.yaml`.
|
| 87 |
+
- **You get:** a **✅ PASS / ❌ FAIL** verdict, two result tables, and a
|
| 88 |
+
downloadable **`validation_report.md`**.
|
| 89 |
+
- **Next:** fix any **❌** gate failures and re-validate; **⚠️** findings are
|
| 90 |
+
advisory.
|
| 91 |
|
| 92 |
---
|
| 93 |
|
| 94 |
+
### 📊 How to read the report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
| Badge | Meaning |
|
| 97 |
+
|---|---|
|
| 98 |
+
| ❌ **Gate failure** | **Must fix before upload** — bad config, broken file paths, a baseline that crashes in Docker. |
|
| 99 |
+
| ⚠️ **Finding** | Advisory design risk, with a citation. Doesn't block upload. |
|
| 100 |
+
| 📋 **Attestation** | A criterion only a human can certify. |
|
| 101 |
+
| • **Skipped** | Couldn't run here (e.g. no Docker) — not a failure. |
|
| 102 |
|
| 103 |
+
Everything downloads from the **workspace panel**: the plan, `bundle.zip`,
|
| 104 |
+
`validation_report.md`, and a combined `workspace.zip`.
|
| 105 |
|
| 106 |
---
|
| 107 |
|
| 108 |
+
<div style="font-size:12px;color:hsl(var(--muted-foreground))">
|
| 109 |
+
⚙ Change the model & theme from <b>settings</b> (top-right) · 💸 each session has a spend cap (default $5) · 🐳 without Docker, baseline-execution checks are skipped, not failed.
|
| 110 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -35,31 +35,40 @@
|
|
| 35 |
|
| 36 |
const INIT_BANNER_HTML = `
|
| 37 |
<span class="ac-init-spinner" aria-hidden="true"></span>
|
| 38 |
-
<span>
|
| 39 |
-
<b>Initializing AutoCodabench…</b>
|
| 40 |
-
spinning up MCP tool servers and the literature index — this takes
|
| 41 |
-
up to 30s on first connect. Chat input is locked until ready.
|
| 42 |
-
</span>
|
| 43 |
`;
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
// ---------------------------------------------------------------
|
| 46 |
// (4) Init banner + input lock, applied from the *very first*
|
| 47 |
// paint so the user never sees an unlocked chat.
|
| 48 |
//
|
| 49 |
-
// We gate
|
| 50 |
-
//
|
| 51 |
-
// and the chat surface is visible.
|
| 52 |
//
|
| 53 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
// ---------------------------------------------------------------
|
| 55 |
function syncInitGate() {
|
| 56 |
const onChatPage = !!document.querySelector("textarea");
|
| 57 |
const bodyText = document.body.textContent;
|
| 58 |
-
const
|
|
|
|
| 59 |
|
| 60 |
// -- banner --
|
| 61 |
let banner = document.getElementById("ac-init-banner");
|
| 62 |
-
if (
|
| 63 |
if (!banner) {
|
| 64 |
banner = document.createElement("div");
|
| 65 |
banner.id = "ac-init-banner";
|
|
@@ -72,7 +81,7 @@
|
|
| 72 |
|
| 73 |
if (!onChatPage) return; // login page: skip input lock too
|
| 74 |
|
| 75 |
-
const locked = !
|
| 76 |
|
| 77 |
// -- textarea --
|
| 78 |
document.querySelectorAll("textarea").forEach((el) => {
|
|
@@ -224,6 +233,82 @@
|
|
| 224 |
setTimeout(() => btn.classList.remove("ac-readme-flash"), 3200);
|
| 225 |
}
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
async function _refreshPhasePillsFromState() {
|
| 228 |
const sid = _currentSessionId();
|
| 229 |
if (!sid) return;
|
|
@@ -238,6 +323,9 @@
|
|
| 238 |
if (!r.ok) return;
|
| 239 |
const state = await r.json();
|
| 240 |
|
|
|
|
|
|
|
|
|
|
| 241 |
// Cache the input mode for syncInputMode() (attach-only lock).
|
| 242 |
window.__acInputMode = state.input_mode || "normal";
|
| 243 |
|
|
@@ -246,7 +334,9 @@
|
|
| 246 |
const sig = JSON.stringify({
|
| 247 |
cur: state.current,
|
| 248 |
mode: window.__acInputMode,
|
| 249 |
-
|
|
|
|
|
|
|
| 250 |
});
|
| 251 |
if (sig === _lastPhasePillsSig) return;
|
| 252 |
_lastPhasePillsSig = sig;
|
|
@@ -258,21 +348,52 @@
|
|
| 258 |
skipped: "Skipped (you started later in the pipeline)",
|
| 259 |
pending: "Upcoming",
|
| 260 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
pillsHost.innerHTML = "";
|
| 262 |
-
|
| 263 |
const pill = document.createElement("span");
|
| 264 |
pill.className = "ac-pp ac-pp-" + ph.status;
|
| 265 |
pill.dataset.phaseId = ph.id;
|
| 266 |
pill.dataset.phaseStatus = ph.status;
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
pill.classList.add("ac-pp-hint");
|
| 277 |
pill.addEventListener("click", _flashReadmeForHelp);
|
| 278 |
}
|
|
@@ -308,7 +429,11 @@
|
|
| 308 |
// stale input_mode (the bug where the validate-mode lock leaks across
|
| 309 |
// New Chat, or fails to apply). On change we drop per-session caches.
|
| 310 |
const text = document.body.textContent || "";
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
let sid = null;
|
| 313 |
if (matches && matches.length) {
|
| 314 |
const m = matches[matches.length - 1].match(/([a-f0-9]{8,16})/);
|
|
@@ -327,6 +452,9 @@
|
|
| 327 |
_lastDownloadsSig = "";
|
| 328 |
for (const k in _lastTagByUrl) delete _lastTagByUrl[k];
|
| 329 |
window.__acPendingModeFetch = true;
|
|
|
|
|
|
|
|
|
|
| 330 |
}
|
| 331 |
return window.__acSessionId || null;
|
| 332 |
}
|
|
@@ -664,6 +792,219 @@
|
|
| 664 |
if (btn.innerHTML !== expected) btn.innerHTML = expected;
|
| 665 |
}
|
| 666 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
function tick() {
|
| 668 |
syncInitGate(); // run first so the lock is up before anything else
|
| 669 |
_currentSessionId(); // detect New-Chat session swap early (resets caches)
|
|
@@ -676,6 +1017,8 @@
|
|
| 676 |
syncInputMode(); // apply the attach-only / locked composer mode
|
| 677 |
_injectSidePanel(); // sci-space-style persistent workspace panel
|
| 678 |
_ensurePhasePills(); // header-row phase pills (slim, no chips)
|
|
|
|
|
|
|
| 679 |
// syncFilesToggle is now redundant — the persistent panel is
|
| 680 |
// the primary file viewer. Keep the function around for the
|
| 681 |
// edge case where the panel can't materialise (no session id).
|
|
|
|
| 35 |
|
| 36 |
const INIT_BANNER_HTML = `
|
| 37 |
<span class="ac-init-spinner" aria-hidden="true"></span>
|
| 38 |
+
<span><b>Starting up…</b> connecting tools — this takes a few seconds.</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
`;
|
| 40 |
|
| 41 |
+
// Chooser is on screen once the entry cards are tagged (or its heading
|
| 42 |
+
// text is present). The server runs the MCP probe BEFORE showing the
|
| 43 |
+
// chooser, so by the time it appears initialization is actually done —
|
| 44 |
+
// the banner should be gone even though no greeting phrase exists yet.
|
| 45 |
+
function _chooserShown(bodyText) {
|
| 46 |
+
return !!document.querySelector("button[data-ac-entry]") ||
|
| 47 |
+
bodyText.includes("Choose how you'd like to start");
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
// ---------------------------------------------------------------
|
| 51 |
// (4) Init banner + input lock, applied from the *very first*
|
| 52 |
// paint so the user never sees an unlocked chat.
|
| 53 |
//
|
| 54 |
+
// We gate on `textarea` existing — the login page has no textarea, so the
|
| 55 |
+
// banner only shows after sign-in.
|
|
|
|
| 56 |
//
|
| 57 |
+
// Banner: shows ONLY while genuinely initializing — i.e. before EITHER the
|
| 58 |
+
// chooser appears (MCP probe done) OR a greeting lands. It used to persist
|
| 59 |
+
// through the whole chooser because it only watched the greeting phrases.
|
| 60 |
+
// Input lock: stays on until a greeting lands, so on the landing the user
|
| 61 |
+
// picks via the cards rather than typing.
|
| 62 |
// ---------------------------------------------------------------
|
| 63 |
function syncInitGate() {
|
| 64 |
const onChatPage = !!document.querySelector("textarea");
|
| 65 |
const bodyText = document.body.textContent;
|
| 66 |
+
const greetingReady = READY_PHRASES.some((p) => bodyText.includes(p));
|
| 67 |
+
const initializing = onChatPage && !greetingReady && !_chooserShown(bodyText);
|
| 68 |
|
| 69 |
// -- banner --
|
| 70 |
let banner = document.getElementById("ac-init-banner");
|
| 71 |
+
if (initializing) {
|
| 72 |
if (!banner) {
|
| 73 |
banner = document.createElement("div");
|
| 74 |
banner.id = "ac-init-banner";
|
|
|
|
| 81 |
|
| 82 |
if (!onChatPage) return; // login page: skip input lock too
|
| 83 |
|
| 84 |
+
const locked = !greetingReady;
|
| 85 |
|
| 86 |
// -- textarea --
|
| 87 |
document.querySelectorAll("textarea").forEach((el) => {
|
|
|
|
| 233 |
setTimeout(() => btn.classList.remove("ac-readme-flash"), 3200);
|
| 234 |
}
|
| 235 |
|
| 236 |
+
// ---- phase pill: custom instant tooltip + click-to-advance ----
|
| 237 |
+
function _acPillTipEl() {
|
| 238 |
+
let t = document.getElementById("ac-pill-tip");
|
| 239 |
+
if (!t) {
|
| 240 |
+
t = document.createElement("div");
|
| 241 |
+
t.id = "ac-pill-tip";
|
| 242 |
+
t.hidden = true;
|
| 243 |
+
document.body.appendChild(t);
|
| 244 |
+
}
|
| 245 |
+
return t;
|
| 246 |
+
}
|
| 247 |
+
function _acShowPillTip(pill) {
|
| 248 |
+
const t = _acPillTipEl();
|
| 249 |
+
t.textContent = pill.dataset.tip || "";
|
| 250 |
+
t.hidden = false;
|
| 251 |
+
const r = pill.getBoundingClientRect();
|
| 252 |
+
const w = t.offsetWidth || 240;
|
| 253 |
+
let left = r.left + r.width / 2 - w / 2;
|
| 254 |
+
left = Math.max(8, Math.min(left, window.innerWidth - w - 8));
|
| 255 |
+
t.style.left = `${Math.round(left)}px`;
|
| 256 |
+
t.style.top = `${Math.round(r.bottom + 8)}px`;
|
| 257 |
+
}
|
| 258 |
+
function _acHidePillTip() {
|
| 259 |
+
const t = document.getElementById("ac-pill-tip");
|
| 260 |
+
if (t) t.hidden = true;
|
| 261 |
+
}
|
| 262 |
+
// Advance by click-simulating the in-chat "▶ Proceed to Phase N" button.
|
| 263 |
+
function _acAdvanceToPhase(num) {
|
| 264 |
+
const wanted = `Proceed to Phase ${num}`;
|
| 265 |
+
for (const b of document.querySelectorAll("button, a")) {
|
| 266 |
+
if ((b.textContent || "").includes(wanted)) {
|
| 267 |
+
try { b.scrollIntoView({block: "center"}); } catch (e) {}
|
| 268 |
+
try { b.click(); } catch (e) {}
|
| 269 |
+
return;
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
_flashReadmeForHelp(); // button not present yet — nudge to the Readme
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
// ---------------------------------------------------------------
|
| 276 |
+
// Persistent cost / context widget (bottom-left). Fed by the
|
| 277 |
+
// phase_state.json poll (state.cost + state.context), refreshed by
|
| 278 |
+
// the server after every turn — so the session spend and context use
|
| 279 |
+
// are ALWAYS visible instead of buried in a per-turn footer line.
|
| 280 |
+
// ---------------------------------------------------------------
|
| 281 |
+
function _renderCostWidget(state) {
|
| 282 |
+
if (!document.querySelector("textarea")) return; // login page
|
| 283 |
+
const cost = state && state.cost;
|
| 284 |
+
const ctx = state && state.context;
|
| 285 |
+
if (!cost) return;
|
| 286 |
+
let w = document.getElementById("ac-cost-widget");
|
| 287 |
+
if (!w) {
|
| 288 |
+
w = document.createElement("div");
|
| 289 |
+
w.id = "ac-cost-widget";
|
| 290 |
+
w.innerHTML =
|
| 291 |
+
'<div class="ac-cost-top">' +
|
| 292 |
+
'<span class="ac-cost-budget"></span>' +
|
| 293 |
+
'<span class="ac-cost-bar"><i></i></span>' +
|
| 294 |
+
'</div>' +
|
| 295 |
+
'<div class="ac-cost-ctx"></div>';
|
| 296 |
+
document.body.appendChild(w);
|
| 297 |
+
}
|
| 298 |
+
const usd = cost.cumulative_usd || 0;
|
| 299 |
+
const budget = cost.budget_usd || 0;
|
| 300 |
+
const pct = Math.max(0, Math.min(100, cost.pct || 0));
|
| 301 |
+
const ctxPct = ctx ? (ctx.pct || 0) : 0;
|
| 302 |
+
w.querySelector(".ac-cost-budget").textContent =
|
| 303 |
+
`💰 $${usd.toFixed(2)} / $${budget.toFixed(2)}`;
|
| 304 |
+
const bar = w.querySelector(".ac-cost-bar > i");
|
| 305 |
+
bar.style.width = pct + "%";
|
| 306 |
+
bar.style.background = pct > 85 ? "hsl(0 72% 52%)"
|
| 307 |
+
: pct > 60 ? "hsl(38 92% 50%)" : "hsl(var(--primary))";
|
| 308 |
+
w.querySelector(".ac-cost-ctx").textContent =
|
| 309 |
+
`🧠 ${ctxPct.toFixed(1)}% context`;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
async function _refreshPhasePillsFromState() {
|
| 313 |
const sid = _currentSessionId();
|
| 314 |
if (!sid) return;
|
|
|
|
| 323 |
if (!r.ok) return;
|
| 324 |
const state = await r.json();
|
| 325 |
|
| 326 |
+
// Always-visible cost / context widget.
|
| 327 |
+
_renderCostWidget(state);
|
| 328 |
+
|
| 329 |
// Cache the input mode for syncInputMode() (attach-only lock).
|
| 330 |
window.__acInputMode = state.input_mode || "normal";
|
| 331 |
|
|
|
|
| 334 |
const sig = JSON.stringify({
|
| 335 |
cur: state.current,
|
| 336 |
mode: window.__acInputMode,
|
| 337 |
+
// include `exists` — the advance pill turns on when the active
|
| 338 |
+
// phase's artifact appears, so a status-only sig would miss it.
|
| 339 |
+
items: (state.phases || []).map((x) => [x.id, x.status, x.exists]),
|
| 340 |
});
|
| 341 |
if (sig === _lastPhasePillsSig) return;
|
| 342 |
_lastPhasePillsSig = sig;
|
|
|
|
| 348 |
skipped: "Skipped (you started later in the pipeline)",
|
| 349 |
pending: "Upcoming",
|
| 350 |
};
|
| 351 |
+
// 1–2 sentence explanation of each phase, shown on hover. Keyed by
|
| 352 |
+
// phase id (config.py: plan / bundle / validate).
|
| 353 |
+
const PHASE_DESC = {
|
| 354 |
+
plan: "Shape your idea into a one-page competition plan "
|
| 355 |
+
+ "(task, data, metric, baseline, rules, ethics, schedule).",
|
| 356 |
+
bundle: "A fresh agent turns the plan into a full Codabench "
|
| 357 |
+
+ "bundle and runs the baseline in Docker to prove it works.",
|
| 358 |
+
validate: "Run the checks plus a Docker baseline execution, then "
|
| 359 |
+
+ "get a PASS/FAIL report.",
|
| 360 |
+
};
|
| 361 |
+
const phases = state.phases || [];
|
| 362 |
+
// The next phase becomes enterable once the active phase's artifact
|
| 363 |
+
// exists (e.g. the plan is saved → you can proceed to Phase 2).
|
| 364 |
+
const activeIdx = phases.findIndex((p) => p.status === "active");
|
| 365 |
+
const advanceIdx = (activeIdx >= 0 && phases[activeIdx].exists)
|
| 366 |
+
? activeIdx + 1 : -1;
|
| 367 |
+
|
| 368 |
pillsHost.innerHTML = "";
|
| 369 |
+
phases.forEach((ph, idx) => {
|
| 370 |
const pill = document.createElement("span");
|
| 371 |
pill.className = "ac-pp ac-pp-" + ph.status;
|
| 372 |
pill.dataset.phaseId = ph.id;
|
| 373 |
pill.dataset.phaseStatus = ph.status;
|
| 374 |
+
const isAdvance = idx === advanceIdx;
|
| 375 |
+
pill.textContent = `${idx + 1}. ${ph.title}`
|
| 376 |
+
+ (isAdvance ? " ▸" : (ICON[ph.status] || ""));
|
| 377 |
+
const desc = PHASE_DESC[ph.id] || ph.title;
|
| 378 |
+
|
| 379 |
+
// Custom instant tooltip (native `title` is delayed/unreliable).
|
| 380 |
+
pill.dataset.tip = isAdvance
|
| 381 |
+
? `${desc}\n\n▸ Click to proceed to ${ph.title}.`
|
| 382 |
+
: (ph.status === "active"
|
| 383 |
+
? `${desc}\n\n● In progress.`
|
| 384 |
+
: `${desc}\n\n${TIP[ph.status] || "Upcoming"}.`);
|
| 385 |
+
pill.addEventListener("mouseenter", () => _acShowPillTip(pill));
|
| 386 |
+
pill.addEventListener("mouseleave", _acHidePillTip);
|
| 387 |
+
|
| 388 |
+
if (isAdvance) {
|
| 389 |
+
// Clickable: proceed to this phase.
|
| 390 |
+
pill.classList.add("ac-pp-advance");
|
| 391 |
+
pill.addEventListener("click", () => {
|
| 392 |
+
_acHidePillTip();
|
| 393 |
+
_acAdvanceToPhase(idx + 1);
|
| 394 |
+
});
|
| 395 |
+
} else if (ph.status !== "active") {
|
| 396 |
+
// Progress-only — clicking nudges the Readme.
|
| 397 |
pill.classList.add("ac-pp-hint");
|
| 398 |
pill.addEventListener("click", _flashReadmeForHelp);
|
| 399 |
}
|
|
|
|
| 429 |
// stale input_mode (the bug where the validate-mode lock leaks across
|
| 430 |
// New Chat, or fails to apply). On change we drop per-session caches.
|
| 431 |
const text = document.body.textContent || "";
|
| 432 |
+
// `\s*` (not `\s+`): the greeting footer renders the id in a chip as
|
| 433 |
+
// "session" + "<hex>" with no whitespace between the two elements, so
|
| 434 |
+
// textContent reads "session1dffc8eeffae". Older "session `hex`" prose
|
| 435 |
+
// still matches too.
|
| 436 |
+
const matches = text.match(/session\s*`?[a-f0-9]{8,16}`?/g);
|
| 437 |
let sid = null;
|
| 438 |
if (matches && matches.length) {
|
| 439 |
const m = matches[matches.length - 1].match(/([a-f0-9]{8,16})/);
|
|
|
|
| 452 |
_lastDownloadsSig = "";
|
| 453 |
for (const k in _lastTagByUrl) delete _lastTagByUrl[k];
|
| 454 |
window.__acPendingModeFetch = true;
|
| 455 |
+
// Reset the cost widget — it re-populates from the new session's
|
| 456 |
+
// first phase_state poll.
|
| 457 |
+
document.getElementById("ac-cost-widget")?.remove();
|
| 458 |
}
|
| 459 |
return window.__acSessionId || null;
|
| 460 |
}
|
|
|
|
| 792 |
if (btn.innerHTML !== expected) btn.innerHTML = expected;
|
| 793 |
}
|
| 794 |
|
| 795 |
+
// ---------------------------------------------------------------
|
| 796 |
+
// (8) Top-right Settings menu.
|
| 797 |
+
//
|
| 798 |
+
// Chainlit ships two separate header controls: a standalone theme
|
| 799 |
+
// toggle (#theme-toggle) and an avatar/user menu (#user-nav-button
|
| 800 |
+
// → Settings + Logout). We fold both into ONE gear dropdown so the
|
| 801 |
+
// top-right has a single, tidy settings affordance with:
|
| 802 |
+
// - a Light / Dark / System segmented switch, and
|
| 803 |
+
// - a Logout row.
|
| 804 |
+
// The natives are hidden via login.css. Theme is applied by
|
| 805 |
+
// mirroring Chainlit's own mechanism (toggle `dark` on <html> +
|
| 806 |
+
// persist localStorage "theme"), verified to stick without a reload.
|
| 807 |
+
// ---------------------------------------------------------------
|
| 808 |
+
|
| 809 |
+
const AC_ICON = {
|
| 810 |
+
gear: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
|
| 811 |
+
sun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/></svg>',
|
| 812 |
+
moon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>',
|
| 813 |
+
system: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
|
| 814 |
+
logout: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/></svg>',
|
| 815 |
+
readme: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
| 816 |
+
};
|
| 817 |
+
|
| 818 |
+
function _acCurrentTheme() {
|
| 819 |
+
const v = localStorage.getItem("theme");
|
| 820 |
+
return (v === "light" || v === "dark" || v === "system") ? v : "system";
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
function _acApplyTheme(mode) {
|
| 824 |
+
localStorage.setItem("theme", mode);
|
| 825 |
+
const dark = mode === "dark" ||
|
| 826 |
+
(mode === "system" &&
|
| 827 |
+
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
| 828 |
+
document.documentElement.classList.toggle("dark", dark);
|
| 829 |
+
// Reflect selection in the segmented control.
|
| 830 |
+
document.querySelectorAll(".ac-theme-opt").forEach((o) => {
|
| 831 |
+
o.classList.toggle("ac-active", o.dataset.mode === mode);
|
| 832 |
+
});
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
function _acLogout() {
|
| 836 |
+
// POST /logout clears the httpOnly auth cookie server-side; then
|
| 837 |
+
// we bounce to /login (requireLogin forces a fresh sign-in).
|
| 838 |
+
fetch("/logout", {method: "POST", credentials: "same-origin"})
|
| 839 |
+
.catch(() => {})
|
| 840 |
+
.finally(() => { window.location.href = "/login"; });
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
function _acCloseSettingsMenu() {
|
| 844 |
+
const menu = document.getElementById("ac-settings-menu");
|
| 845 |
+
const btn = document.getElementById("ac-settings-btn");
|
| 846 |
+
if (menu) menu.hidden = true;
|
| 847 |
+
if (btn) btn.setAttribute("aria-expanded", "false");
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
function _acPositionSettingsMenu() {
|
| 851 |
+
const menu = document.getElementById("ac-settings-menu");
|
| 852 |
+
const btn = document.getElementById("ac-settings-btn");
|
| 853 |
+
if (!menu || !btn || menu.hidden) return;
|
| 854 |
+
const r = btn.getBoundingClientRect();
|
| 855 |
+
const w = menu.offsetWidth || 248;
|
| 856 |
+
let left = r.right - w; // right-align to the gear
|
| 857 |
+
left = Math.max(8, Math.min(left, window.innerWidth - w - 8));
|
| 858 |
+
menu.style.top = `${Math.round(r.bottom + 8)}px`;
|
| 859 |
+
menu.style.left = `${Math.round(left)}px`;
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
function _acFillUserName() {
|
| 863 |
+
// Best-effort: show who's signed in. Cached after first fetch.
|
| 864 |
+
if (window.__acUserFetched) return;
|
| 865 |
+
window.__acUserFetched = true;
|
| 866 |
+
fetch("/user", {credentials: "same-origin"})
|
| 867 |
+
.then((r) => (r.ok ? r.json() : null))
|
| 868 |
+
.then((u) => {
|
| 869 |
+
if (!u) return;
|
| 870 |
+
const name = u.display_name || u.identifier || "Signed in";
|
| 871 |
+
const nameEl = document.querySelector(".ac-set-user-name");
|
| 872 |
+
const avEl = document.querySelector(".ac-set-avatar");
|
| 873 |
+
if (nameEl) nameEl.textContent = name;
|
| 874 |
+
if (avEl) avEl.textContent = (name[0] || "?").toUpperCase();
|
| 875 |
+
})
|
| 876 |
+
.catch(() => {});
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
function _ensureSettingsMenu() {
|
| 880 |
+
if (!document.querySelector("textarea")) return; // login page
|
| 881 |
+
|
| 882 |
+
// (a) the dropdown card — mounted once on <body> so header
|
| 883 |
+
// re-renders never blow it away.
|
| 884 |
+
let menu = document.getElementById("ac-settings-menu");
|
| 885 |
+
if (!menu) {
|
| 886 |
+
const cur = _acCurrentTheme();
|
| 887 |
+
const opt = (mode, icon, label) =>
|
| 888 |
+
`<button type="button" class="ac-theme-opt${mode === cur ? " ac-active" : ""}" ` +
|
| 889 |
+
`data-mode="${mode}" title="${label} theme">${icon}<span>${label}</span></button>`;
|
| 890 |
+
menu = document.createElement("div");
|
| 891 |
+
menu.id = "ac-settings-menu";
|
| 892 |
+
menu.hidden = true;
|
| 893 |
+
menu.innerHTML =
|
| 894 |
+
`<div class="ac-set-user">` +
|
| 895 |
+
`<span class="ac-set-avatar">?</span>` +
|
| 896 |
+
`<span class="ac-set-user-meta">` +
|
| 897 |
+
`<span class="ac-set-user-name">Signed in</span>` +
|
| 898 |
+
`<span class="ac-set-user-sub">AutoCodabench</span>` +
|
| 899 |
+
`</span>` +
|
| 900 |
+
`</div>` +
|
| 901 |
+
`<div class="ac-set-label">Appearance</div>` +
|
| 902 |
+
`<div class="ac-theme-seg">` +
|
| 903 |
+
opt("light", AC_ICON.sun, "Light") +
|
| 904 |
+
opt("dark", AC_ICON.moon, "Dark") +
|
| 905 |
+
opt("system", AC_ICON.system, "System") +
|
| 906 |
+
`</div>` +
|
| 907 |
+
`<div class="ac-set-sep"></div>` +
|
| 908 |
+
`<button type="button" class="ac-set-row" id="ac-readme-row">` +
|
| 909 |
+
`${AC_ICON.readme}<span>Readme</span></button>` +
|
| 910 |
+
`<button type="button" class="ac-set-row ac-set-danger" id="ac-logout-row">` +
|
| 911 |
+
`${AC_ICON.logout}<span>Log out</span></button>`;
|
| 912 |
+
document.body.appendChild(menu);
|
| 913 |
+
|
| 914 |
+
menu.querySelectorAll(".ac-theme-opt").forEach((o) => {
|
| 915 |
+
o.addEventListener("click", (e) => {
|
| 916 |
+
e.stopPropagation();
|
| 917 |
+
_acApplyTheme(o.dataset.mode);
|
| 918 |
+
});
|
| 919 |
+
});
|
| 920 |
+
// Readme: delegate to the native (now hidden) Readme button.
|
| 921 |
+
menu.querySelector("#ac-readme-row").addEventListener("click", (e) => {
|
| 922 |
+
e.stopPropagation();
|
| 923 |
+
_acCloseSettingsMenu();
|
| 924 |
+
const rb = document.getElementById("readme-button");
|
| 925 |
+
if (rb) rb.click();
|
| 926 |
+
});
|
| 927 |
+
menu.querySelector("#ac-logout-row")
|
| 928 |
+
.addEventListener("click", (e) => { e.stopPropagation(); _acLogout(); });
|
| 929 |
+
// Keep clicks inside the menu from bubbling to the document
|
| 930 |
+
// close-handler.
|
| 931 |
+
menu.addEventListener("click", (e) => e.stopPropagation());
|
| 932 |
+
_acFillUserName();
|
| 933 |
+
// Sync the active indicator to the live theme on first build.
|
| 934 |
+
_acApplyTheme(_acCurrentTheme());
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
// (b) the gear button. Anchor it right AFTER the phase pills when they
|
| 938 |
+
// exist — the user always sees the pills, so that's the most
|
| 939 |
+
// reliable spot and puts Settings next to the phase bar. On the
|
| 940 |
+
// landing (no pills yet) fall back to just before the hidden Readme
|
| 941 |
+
// button. Re-placed whenever Chainlit's re-render moves things.
|
| 942 |
+
const pills = document.getElementById("ac-phase-pills");
|
| 943 |
+
const readme = _findReadmeButton();
|
| 944 |
+
if (pills || (readme && readme.parentElement)) {
|
| 945 |
+
let btn = document.getElementById("ac-settings-btn");
|
| 946 |
+
if (!btn) {
|
| 947 |
+
btn = document.createElement("button");
|
| 948 |
+
btn.id = "ac-settings-btn";
|
| 949 |
+
btn.type = "button";
|
| 950 |
+
btn.setAttribute("aria-label", "Settings");
|
| 951 |
+
btn.setAttribute("aria-haspopup", "menu");
|
| 952 |
+
btn.setAttribute("aria-expanded", "false");
|
| 953 |
+
btn.title = "Settings";
|
| 954 |
+
btn.innerHTML = AC_ICON.gear;
|
| 955 |
+
btn.addEventListener("click", (e) => {
|
| 956 |
+
e.stopPropagation();
|
| 957 |
+
const m = document.getElementById("ac-settings-menu");
|
| 958 |
+
if (!m) return;
|
| 959 |
+
const willOpen = m.hidden;
|
| 960 |
+
m.hidden = !willOpen;
|
| 961 |
+
btn.setAttribute("aria-expanded", String(willOpen));
|
| 962 |
+
if (willOpen) { _acFillUserName(); _acPositionSettingsMenu(); }
|
| 963 |
+
});
|
| 964 |
+
}
|
| 965 |
+
if (pills) {
|
| 966 |
+
// BEFORE the pills (to their left) — keeps the gear clear of
|
| 967 |
+
// the fixed workspace sliver that hugs the right edge and would
|
| 968 |
+
// otherwise cover it.
|
| 969 |
+
if (pills.previousElementSibling !== btn) {
|
| 970 |
+
pills.insertAdjacentElement("beforebegin", btn);
|
| 971 |
+
}
|
| 972 |
+
} else if (readme.previousElementSibling !== btn) {
|
| 973 |
+
readme.parentElement.insertBefore(btn, readme);
|
| 974 |
+
}
|
| 975 |
+
}
|
| 976 |
+
|
| 977 |
+
// One-time global wiring: outside-click + Escape close, reposition.
|
| 978 |
+
if (!window.__acSettingsWired) {
|
| 979 |
+
window.__acSettingsWired = true;
|
| 980 |
+
document.addEventListener("click", () => _acCloseSettingsMenu());
|
| 981 |
+
document.addEventListener("keydown", (e) => {
|
| 982 |
+
if (e.key === "Escape") _acCloseSettingsMenu();
|
| 983 |
+
});
|
| 984 |
+
window.addEventListener("resize", _acPositionSettingsMenu);
|
| 985 |
+
window.addEventListener("scroll", _acPositionSettingsMenu, true);
|
| 986 |
+
}
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
// ---------------------------------------------------------------
|
| 990 |
+
// (9) Landing chooser cards.
|
| 991 |
+
//
|
| 992 |
+
// The two entry options are cl.Action buttons (server: _ask_entry_mode).
|
| 993 |
+
// We tag each by its (clean, fixed) label so login.css can render them
|
| 994 |
+
// as big product-style cards with an icon + description, instead of the
|
| 995 |
+
// default emoji chat buttons. Idempotent; safe to run every tick.
|
| 996 |
+
// ---------------------------------------------------------------
|
| 997 |
+
const _AC_ENTRY = {
|
| 998 |
+
"Create from scratch": "create",
|
| 999 |
+
"Validate a bundle": "validate",
|
| 1000 |
+
};
|
| 1001 |
+
function _tagEntryCards() {
|
| 1002 |
+
document.querySelectorAll("button").forEach((el) => {
|
| 1003 |
+
const mode = _AC_ENTRY[(el.textContent || "").trim()];
|
| 1004 |
+
if (mode && el.dataset.acEntry !== mode) el.dataset.acEntry = mode;
|
| 1005 |
+
});
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
function tick() {
|
| 1009 |
syncInitGate(); // run first so the lock is up before anything else
|
| 1010 |
_currentSessionId(); // detect New-Chat session swap early (resets caches)
|
|
|
|
| 1017 |
syncInputMode(); // apply the attach-only / locked composer mode
|
| 1018 |
_injectSidePanel(); // sci-space-style persistent workspace panel
|
| 1019 |
_ensurePhasePills(); // header-row phase pills (slim, no chips)
|
| 1020 |
+
_ensureSettingsMenu(); // top-right gear: theme switch + logout
|
| 1021 |
+
_tagEntryCards(); // landing: tag the two entry options as cards
|
| 1022 |
// syncFilesToggle is now redundant — the persistent panel is
|
| 1023 |
// the primary file viewer. Keep the function around for the
|
| 1024 |
// edge case where the panel can't materialise (no session id).
|
|
|
Binary file (82.5 kB). View file
|
|
|
|
@@ -60,16 +60,18 @@ form:has(input[type="password"]):not(.ac-pub-form) > *:has(> button[type="submit
|
|
| 60 |
display: flex;
|
| 61 |
align-items: center;
|
| 62 |
justify-content: center;
|
| 63 |
-
gap:
|
| 64 |
-
padding:
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
| 67 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
| 68 |
Roboto, Helvetica, Arial, sans-serif;
|
| 69 |
-
font-size:
|
| 70 |
line-height: 1.4;
|
| 71 |
-
box-shadow: 0 2px
|
| 72 |
-
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
|
| 73 |
/* Slide-down on first appearance so it doesn't feel jarring. */
|
| 74 |
animation: ac-banner-in 0.25s ease-out;
|
| 75 |
}
|
|
@@ -78,14 +80,15 @@ form:has(input[type="password"]):not(.ac-pub-form) > *:has(> button[type="submit
|
|
| 78 |
}
|
| 79 |
#ac-init-banner b {
|
| 80 |
font-weight: 600;
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
.ac-init-spinner {
|
| 84 |
flex: 0 0 auto;
|
| 85 |
-
width:
|
| 86 |
-
height:
|
| 87 |
-
border: 2px solid
|
| 88 |
-
border-top-color:
|
| 89 |
border-radius: 50%;
|
| 90 |
animation: ac-spin 0.9s linear infinite;
|
| 91 |
}
|
|
@@ -106,97 +109,6 @@ textarea.ac-input-locked::placeholder {
|
|
| 106 |
}
|
| 107 |
|
| 108 |
|
| 109 |
-
/* ============================================================
|
| 110 |
-
* Phase-switch action buttons.
|
| 111 |
-
*
|
| 112 |
-
* We can't target our cl.Action buttons by a stable class (Chainlit
|
| 113 |
-
* doesn't expose one), so we match by visible label text — the strings
|
| 114 |
-
* are fixed server-side in app.py's _on_switch_to_impl / etc. handlers.
|
| 115 |
-
* `:has()` is supported in every evergreen browser as of 2023.
|
| 116 |
-
*
|
| 117 |
-
* Three buttons styled:
|
| 118 |
-
* - "START IMPLEMENTATION (irreversible)" — big, orange/red, pulses.
|
| 119 |
-
* - "YES — switch to IMPLEMENTATION" — large, red, no pulse.
|
| 120 |
-
* - "Cancel — keep planning" — large, calm grey.
|
| 121 |
-
* ============================================================ */
|
| 122 |
-
|
| 123 |
-
button:has(span:is(:contains)),
|
| 124 |
-
button[aria-label*="START IMPLEMENTATION" i],
|
| 125 |
-
button:has(*:not(script):not(style)) {
|
| 126 |
-
/* The :has(:contains) selector isn't standard, so we fall through
|
| 127 |
-
and rely on the `text starts with` JS tagger in chat.js — see
|
| 128 |
-
below. The selector above is a no-op safety net for browsers
|
| 129 |
-
that try to parse it; the real styling targets data attributes. */
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
/* Tagged by chat.js: a Chainlit action button whose label starts with
|
| 133 |
-
* "🛠 START IMPLEMENTATION". Pulsing border to drag the eye. */
|
| 134 |
-
button[data-ac-impl-button="primary"] {
|
| 135 |
-
display: inline-flex !important;
|
| 136 |
-
align-items: center;
|
| 137 |
-
justify-content: center;
|
| 138 |
-
min-height: 56px;
|
| 139 |
-
padding: 14px 28px !important;
|
| 140 |
-
margin: 12px 0 !important;
|
| 141 |
-
font-size: 17px !important;
|
| 142 |
-
font-weight: 700 !important;
|
| 143 |
-
letter-spacing: 0.3px;
|
| 144 |
-
color: #fff !important;
|
| 145 |
-
background: linear-gradient(90deg, #d97706 0%, #dc2626 100%) !important;
|
| 146 |
-
border: 2px solid #fff !important;
|
| 147 |
-
border-radius: 10px !important;
|
| 148 |
-
box-shadow:
|
| 149 |
-
0 0 0 0 rgba(220, 38, 38, 0.6),
|
| 150 |
-
0 4px 18px rgba(220, 38, 38, 0.35) !important;
|
| 151 |
-
cursor: pointer !important;
|
| 152 |
-
animation: ac-pulse-cta 1.6s ease-in-out infinite;
|
| 153 |
-
}
|
| 154 |
-
button[data-ac-impl-button="primary"]:hover {
|
| 155 |
-
filter: brightness(1.05);
|
| 156 |
-
transform: translateY(-1px);
|
| 157 |
-
}
|
| 158 |
-
@keyframes ac-pulse-cta {
|
| 159 |
-
0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.6),
|
| 160 |
-
0 4px 18px rgba(220, 38, 38, 0.35); }
|
| 161 |
-
50% { box-shadow: 0 0 0 12px rgba(220, 38, 38, 0.0),
|
| 162 |
-
0 6px 22px rgba(220, 38, 38, 0.45); }
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
/* The final-confirm "YES" button — same loud styling, no pulse (user
|
| 166 |
-
* has already seen the pulse; we want them deliberate now). */
|
| 167 |
-
button[data-ac-impl-button="confirm"] {
|
| 168 |
-
display: inline-flex !important;
|
| 169 |
-
align-items: center;
|
| 170 |
-
justify-content: center;
|
| 171 |
-
min-height: 52px;
|
| 172 |
-
padding: 12px 26px !important;
|
| 173 |
-
margin: 10px 8px 10px 0 !important;
|
| 174 |
-
font-size: 16px !important;
|
| 175 |
-
font-weight: 700 !important;
|
| 176 |
-
color: #fff !important;
|
| 177 |
-
background: #dc2626 !important;
|
| 178 |
-
border: 2px solid #991b1b !important;
|
| 179 |
-
border-radius: 8px !important;
|
| 180 |
-
box-shadow: 0 4px 14px rgba(220, 38, 38, 0.30) !important;
|
| 181 |
-
cursor: pointer !important;
|
| 182 |
-
}
|
| 183 |
-
button[data-ac-impl-button="cancel"] {
|
| 184 |
-
display: inline-flex !important;
|
| 185 |
-
align-items: center;
|
| 186 |
-
justify-content: center;
|
| 187 |
-
min-height: 52px;
|
| 188 |
-
padding: 12px 22px !important;
|
| 189 |
-
margin: 10px 0 !important;
|
| 190 |
-
font-size: 15px !important;
|
| 191 |
-
font-weight: 600 !important;
|
| 192 |
-
color: CanvasText !important;
|
| 193 |
-
background: rgba(127, 127, 127, 0.10) !important;
|
| 194 |
-
border: 2px solid rgba(127, 127, 127, 0.35) !important;
|
| 195 |
-
border-radius: 8px !important;
|
| 196 |
-
cursor: pointer !important;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
|
| 200 |
/* ============================================================
|
| 201 |
* Persistent "📁 Files" floating toggle (chat.js).
|
| 202 |
*
|
|
@@ -336,28 +248,6 @@ textarea.ac-attach-only {
|
|
| 336 |
}
|
| 337 |
|
| 338 |
|
| 339 |
-
/* Hide the cl.Action buttons we use only as click targets for the
|
| 340 |
-
* phase pills. Labels like "AC_ADVANCE::kit" / "AC_REVERT::plan"
|
| 341 |
-
* are tagged in chat.js as data-ac-phase-nav="advance|revert" and
|
| 342 |
-
* hidden here — and so is their host message card via :has(). */
|
| 343 |
-
button[data-ac-phase-nav] {
|
| 344 |
-
position: absolute !important;
|
| 345 |
-
width: 1px !important;
|
| 346 |
-
height: 1px !important;
|
| 347 |
-
padding: 0 !important;
|
| 348 |
-
margin: -1px !important;
|
| 349 |
-
overflow: hidden !important;
|
| 350 |
-
clip: rect(0, 0, 0, 0) !important;
|
| 351 |
-
border: 0 !important;
|
| 352 |
-
opacity: 0 !important;
|
| 353 |
-
pointer-events: auto !important; /* still clickable from JS */
|
| 354 |
-
}
|
| 355 |
-
*:has(> button[data-ac-phase-nav]),
|
| 356 |
-
[class*="message"]:has(button[data-ac-phase-nav]) {
|
| 357 |
-
display: none !important;
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
|
| 361 |
/* ============================================================
|
| 362 |
* Persistent right workspace panel (sci-space style).
|
| 363 |
*
|
|
@@ -374,7 +264,9 @@ button[data-ac-phase-nav] {
|
|
| 374 |
* just offscreen, and might intercept clicks" failure mode. */
|
| 375 |
#ac-side-panel {
|
| 376 |
position: fixed !important;
|
| 377 |
-
|
|
|
|
|
|
|
| 378 |
right: 0 !important;
|
| 379 |
bottom: 0 !important;
|
| 380 |
/* 55vw clamped to [480, 900] px so the notebook has real reading
|
|
@@ -440,26 +332,30 @@ button[data-ac-phase-nav] {
|
|
| 440 |
flex: 1 !important;
|
| 441 |
width: 44px !important;
|
| 442 |
height: 100% !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
border: none !important;
|
|
|
|
| 444 |
border-radius: 0 !important;
|
| 445 |
-
background:
|
| 446 |
-
|
| 447 |
-
color-mix(in oklab, Canvas 82%, currentColor 10%) 100%) !important;
|
| 448 |
-
color: CanvasText !important;
|
| 449 |
cursor: pointer !important;
|
| 450 |
-
font-size:
|
| 451 |
font-weight: 700 !important;
|
| 452 |
letter-spacing: 0.6px !important;
|
| 453 |
writing-mode: vertical-rl !important;
|
| 454 |
text-orientation: mixed !important;
|
| 455 |
padding: 16px 0 !important;
|
| 456 |
line-height: 1.3 !important;
|
|
|
|
| 457 |
}
|
| 458 |
#ac-side-panel[data-state="collapsed"]:hover #ac-side-collapse,
|
| 459 |
#ac-side-panel.ac-collapsed:hover #ac-side-collapse {
|
| 460 |
-
background:
|
| 461 |
-
|
| 462 |
-
color-mix(in oklab, Canvas 70%, currentColor 16%) 100%) !important;
|
| 463 |
}
|
| 464 |
|
| 465 |
.ac-side-header {
|
|
@@ -669,3 +565,375 @@ body.ac-side-active {
|
|
| 669 |
border-left: none;
|
| 670 |
}
|
| 671 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
display: flex;
|
| 61 |
align-items: center;
|
| 62 |
justify-content: center;
|
| 63 |
+
gap: 10px;
|
| 64 |
+
padding: 11px 20px;
|
| 65 |
+
/* Theme-adaptive, neutral — no purple gradient. Reads as a quiet system
|
| 66 |
+
* notification that matches the rest of the UI in light and dark. */
|
| 67 |
+
background: hsl(var(--popover));
|
| 68 |
+
color: hsl(var(--muted-foreground));
|
| 69 |
+
border-bottom: 1px solid hsl(var(--border));
|
| 70 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
| 71 |
Roboto, Helvetica, Arial, sans-serif;
|
| 72 |
+
font-size: 13.5px;
|
| 73 |
line-height: 1.4;
|
| 74 |
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
|
|
|
| 75 |
/* Slide-down on first appearance so it doesn't feel jarring. */
|
| 76 |
animation: ac-banner-in 0.25s ease-out;
|
| 77 |
}
|
|
|
|
| 80 |
}
|
| 81 |
#ac-init-banner b {
|
| 82 |
font-weight: 600;
|
| 83 |
+
color: hsl(var(--foreground));
|
| 84 |
}
|
| 85 |
|
| 86 |
.ac-init-spinner {
|
| 87 |
flex: 0 0 auto;
|
| 88 |
+
width: 15px;
|
| 89 |
+
height: 15px;
|
| 90 |
+
border: 2px solid hsl(var(--border));
|
| 91 |
+
border-top-color: hsl(var(--primary));
|
| 92 |
border-radius: 50%;
|
| 93 |
animation: ac-spin 0.9s linear infinite;
|
| 94 |
}
|
|
|
|
| 109 |
}
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
/* ============================================================
|
| 113 |
* Persistent "📁 Files" floating toggle (chat.js).
|
| 114 |
*
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
/* ============================================================
|
| 252 |
* Persistent right workspace panel (sci-space style).
|
| 253 |
*
|
|
|
|
| 264 |
* just offscreen, and might intercept clicks" failure mode. */
|
| 265 |
#ac-side-panel {
|
| 266 |
position: fixed !important;
|
| 267 |
+
/* Start BELOW the header so the workspace panel/sliver never overlaps the
|
| 268 |
+
* phase bar (it used to cover the right edge and cut off "3. Validation"). */
|
| 269 |
+
top: 56px !important;
|
| 270 |
right: 0 !important;
|
| 271 |
bottom: 0 !important;
|
| 272 |
/* 55vw clamped to [480, 900] px so the notebook has real reading
|
|
|
|
| 332 |
flex: 1 !important;
|
| 333 |
width: 44px !important;
|
| 334 |
height: 100% !important;
|
| 335 |
+
display: flex !important;
|
| 336 |
+
align-items: center !important;
|
| 337 |
+
justify-content: center !important;
|
| 338 |
+
/* Theme tokens (not system Canvas) so the strip matches the app in dark
|
| 339 |
+
* mode instead of showing as a jarring light bar; centered label. */
|
| 340 |
border: none !important;
|
| 341 |
+
border-left: 1px solid hsl(var(--border)) !important;
|
| 342 |
border-radius: 0 !important;
|
| 343 |
+
background: hsl(var(--popover)) !important;
|
| 344 |
+
color: hsl(var(--muted-foreground)) !important;
|
|
|
|
|
|
|
| 345 |
cursor: pointer !important;
|
| 346 |
+
font-size: 12px !important;
|
| 347 |
font-weight: 700 !important;
|
| 348 |
letter-spacing: 0.6px !important;
|
| 349 |
writing-mode: vertical-rl !important;
|
| 350 |
text-orientation: mixed !important;
|
| 351 |
padding: 16px 0 !important;
|
| 352 |
line-height: 1.3 !important;
|
| 353 |
+
transition: background 0.12s ease-out, color 0.12s ease-out !important;
|
| 354 |
}
|
| 355 |
#ac-side-panel[data-state="collapsed"]:hover #ac-side-collapse,
|
| 356 |
#ac-side-panel.ac-collapsed:hover #ac-side-collapse {
|
| 357 |
+
background: hsl(var(--accent)) !important;
|
| 358 |
+
color: hsl(var(--foreground)) !important;
|
|
|
|
| 359 |
}
|
| 360 |
|
| 361 |
.ac-side-header {
|
|
|
|
| 565 |
border-left: none;
|
| 566 |
}
|
| 567 |
}
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
/* ============================================================
|
| 571 |
+
* Top-right Settings menu (injected by chat.js).
|
| 572 |
+
*
|
| 573 |
+
* Consolidates Chainlit's two separate native header controls — the
|
| 574 |
+
* standalone theme toggle (#theme-toggle) and the avatar/user menu
|
| 575 |
+
* (#user-nav-button, which held Settings + Logout) — into a single
|
| 576 |
+
* gear dropdown holding the Light/Dark/System theme switch + Logout.
|
| 577 |
+
* The natives are hidden in favour of this one menu. Colors use
|
| 578 |
+
* Chainlit's own shadcn CSS variables so the menu tracks the active
|
| 579 |
+
* theme automatically in both light and dark.
|
| 580 |
+
* ============================================================ */
|
| 581 |
+
|
| 582 |
+
/* Fold the native controls into our single gear menu. The theme toggle and
|
| 583 |
+
* user/avatar menu are redundant; Readme moves INTO the gear menu but stays
|
| 584 |
+
* in the DOM (display:none) so it remains the click target + layout anchor. */
|
| 585 |
+
#theme-toggle,
|
| 586 |
+
#user-nav-button,
|
| 587 |
+
#readme-button { display: none !important; }
|
| 588 |
+
|
| 589 |
+
/* Gear button — injected into the header flow just LEFT of "Readme"
|
| 590 |
+
* (chat.js). That keeps it clear of both the workspace sliver on the far
|
| 591 |
+
* right and Readme itself, and it inherits the header's vertical alignment.
|
| 592 |
+
* Matches Chainlit's 36px icon-button chrome. */
|
| 593 |
+
#ac-settings-btn {
|
| 594 |
+
/* relative + z-index so the gear always renders ABOVE the fixed workspace
|
| 595 |
+
* sliver (z 10000) even if layout pushes it near the right edge. */
|
| 596 |
+
position: relative;
|
| 597 |
+
z-index: 10001;
|
| 598 |
+
display: inline-flex;
|
| 599 |
+
align-items: center;
|
| 600 |
+
justify-content: center;
|
| 601 |
+
width: 36px;
|
| 602 |
+
height: 36px;
|
| 603 |
+
margin: 0 6px;
|
| 604 |
+
border-radius: 6px;
|
| 605 |
+
border: none;
|
| 606 |
+
background: transparent;
|
| 607 |
+
color: hsl(var(--muted-foreground));
|
| 608 |
+
cursor: pointer;
|
| 609 |
+
transition: background 0.12s ease-out, color 0.12s ease-out;
|
| 610 |
+
}
|
| 611 |
+
#ac-settings-btn:hover,
|
| 612 |
+
#ac-settings-btn[aria-expanded="true"] {
|
| 613 |
+
background: hsl(var(--accent));
|
| 614 |
+
color: hsl(var(--foreground));
|
| 615 |
+
}
|
| 616 |
+
#ac-settings-btn svg { width: 18px; height: 18px; }
|
| 617 |
+
|
| 618 |
+
/* Dropdown card (mounted on <body>, fixed-positioned under the gear). */
|
| 619 |
+
#ac-settings-menu {
|
| 620 |
+
position: fixed;
|
| 621 |
+
z-index: 100000;
|
| 622 |
+
min-width: 248px;
|
| 623 |
+
padding: 8px;
|
| 624 |
+
border-radius: 12px;
|
| 625 |
+
background: hsl(var(--popover));
|
| 626 |
+
color: hsl(var(--popover-foreground));
|
| 627 |
+
border: 1px solid hsl(var(--border));
|
| 628 |
+
box-shadow: 0 12px 34px rgba(0, 0, 0, 0.28);
|
| 629 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
| 630 |
+
Roboto, Helvetica, Arial, sans-serif;
|
| 631 |
+
animation: ac-menu-in 0.12s ease-out;
|
| 632 |
+
}
|
| 633 |
+
#ac-settings-menu[hidden] { display: none; }
|
| 634 |
+
@keyframes ac-menu-in {
|
| 635 |
+
from { opacity: 0; transform: translateY(-4px); }
|
| 636 |
+
to { opacity: 1; transform: translateY(0); }
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
/* Signed-in-as header. */
|
| 640 |
+
.ac-set-user {
|
| 641 |
+
display: flex;
|
| 642 |
+
align-items: center;
|
| 643 |
+
gap: 10px;
|
| 644 |
+
padding: 6px 8px 10px;
|
| 645 |
+
margin-bottom: 6px;
|
| 646 |
+
border-bottom: 1px solid hsl(var(--border));
|
| 647 |
+
}
|
| 648 |
+
.ac-set-avatar {
|
| 649 |
+
flex: 0 0 auto;
|
| 650 |
+
width: 30px;
|
| 651 |
+
height: 30px;
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
display: flex;
|
| 654 |
+
align-items: center;
|
| 655 |
+
justify-content: center;
|
| 656 |
+
background: hsl(var(--primary));
|
| 657 |
+
color: #fff;
|
| 658 |
+
font-weight: 700;
|
| 659 |
+
font-size: 13px;
|
| 660 |
+
}
|
| 661 |
+
.ac-set-user-meta { display: flex; flex-direction: column; min-width: 0; }
|
| 662 |
+
.ac-set-user-name {
|
| 663 |
+
font-size: 13px;
|
| 664 |
+
font-weight: 600;
|
| 665 |
+
color: hsl(var(--popover-foreground));
|
| 666 |
+
white-space: nowrap;
|
| 667 |
+
overflow: hidden;
|
| 668 |
+
text-overflow: ellipsis;
|
| 669 |
+
}
|
| 670 |
+
.ac-set-user-sub { font-size: 11px; color: hsl(var(--muted-foreground)); }
|
| 671 |
+
|
| 672 |
+
.ac-set-label {
|
| 673 |
+
font-size: 10.5px;
|
| 674 |
+
font-weight: 700;
|
| 675 |
+
text-transform: uppercase;
|
| 676 |
+
letter-spacing: 0.6px;
|
| 677 |
+
color: hsl(var(--muted-foreground));
|
| 678 |
+
padding: 2px 8px 6px;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
/* Segmented Light / Dark / System control. */
|
| 682 |
+
.ac-theme-seg {
|
| 683 |
+
display: grid;
|
| 684 |
+
grid-template-columns: repeat(3, 1fr);
|
| 685 |
+
gap: 4px;
|
| 686 |
+
padding: 3px;
|
| 687 |
+
margin: 0 4px 6px;
|
| 688 |
+
background: hsl(var(--accent));
|
| 689 |
+
border-radius: 9px;
|
| 690 |
+
}
|
| 691 |
+
.ac-theme-opt {
|
| 692 |
+
display: flex;
|
| 693 |
+
flex-direction: column;
|
| 694 |
+
align-items: center;
|
| 695 |
+
gap: 4px;
|
| 696 |
+
padding: 8px 4px;
|
| 697 |
+
border: none;
|
| 698 |
+
background: transparent;
|
| 699 |
+
border-radius: 7px;
|
| 700 |
+
cursor: pointer;
|
| 701 |
+
color: hsl(var(--muted-foreground));
|
| 702 |
+
font-size: 11px;
|
| 703 |
+
font-weight: 600;
|
| 704 |
+
transition: background 0.1s ease-out, color 0.1s ease-out;
|
| 705 |
+
}
|
| 706 |
+
.ac-theme-opt svg { width: 16px; height: 16px; }
|
| 707 |
+
.ac-theme-opt:hover { color: hsl(var(--foreground)); }
|
| 708 |
+
.ac-theme-opt.ac-active {
|
| 709 |
+
background: hsl(var(--popover));
|
| 710 |
+
color: hsl(var(--foreground));
|
| 711 |
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.20);
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
.ac-set-sep { height: 1px; background: hsl(var(--border)); margin: 6px 4px; }
|
| 715 |
+
|
| 716 |
+
/* Action rows (Logout). */
|
| 717 |
+
.ac-set-row {
|
| 718 |
+
display: flex;
|
| 719 |
+
align-items: center;
|
| 720 |
+
gap: 10px;
|
| 721 |
+
width: calc(100% - 8px);
|
| 722 |
+
margin: 0 4px;
|
| 723 |
+
padding: 9px 10px;
|
| 724 |
+
border: none;
|
| 725 |
+
background: transparent;
|
| 726 |
+
border-radius: 8px;
|
| 727 |
+
cursor: pointer;
|
| 728 |
+
text-align: left;
|
| 729 |
+
color: hsl(var(--popover-foreground));
|
| 730 |
+
font-size: 13px;
|
| 731 |
+
font-weight: 500;
|
| 732 |
+
}
|
| 733 |
+
.ac-set-row svg { flex: 0 0 auto; width: 16px; height: 16px; }
|
| 734 |
+
.ac-set-row:hover { background: hsl(var(--accent)); }
|
| 735 |
+
.ac-set-row.ac-set-danger { color: hsl(var(--destructive)); }
|
| 736 |
+
.ac-set-row.ac-set-danger:hover {
|
| 737 |
+
background: color-mix(in oklab, hsl(var(--destructive)) 14%, transparent);
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
/* ============================================================
|
| 742 |
+
* Landing chooser cards (chat.js tags the two entry buttons
|
| 743 |
+
* data-ac-entry="create|validate"). The cl.Action label is the
|
| 744 |
+
* card title; the icon (::before) + description (::after) are
|
| 745 |
+
* added here so the first screen reads like a product chooser
|
| 746 |
+
* rather than two emoji chat buttons.
|
| 747 |
+
* ============================================================ */
|
| 748 |
+
|
| 749 |
+
/* Lay the two cards out side by side, centered. Chainlit wraps the
|
| 750 |
+
* AskActionMessage buttons in a flex container — promote whichever
|
| 751 |
+
* element directly holds them to a centered, wrapping row. */
|
| 752 |
+
div:has(> button[data-ac-entry]),
|
| 753 |
+
div:has(> * > button[data-ac-entry]) {
|
| 754 |
+
display: flex !important;
|
| 755 |
+
flex-wrap: wrap;
|
| 756 |
+
gap: 16px;
|
| 757 |
+
justify-content: center;
|
| 758 |
+
align-items: stretch;
|
| 759 |
+
margin: 10px 0 6px;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
button[data-ac-entry] {
|
| 763 |
+
display: flex !important;
|
| 764 |
+
flex-direction: column !important;
|
| 765 |
+
align-items: flex-start !important;
|
| 766 |
+
justify-content: flex-start !important;
|
| 767 |
+
gap: 10px !important;
|
| 768 |
+
width: 300px !important;
|
| 769 |
+
max-width: 44vw !important;
|
| 770 |
+
min-height: 158px !important;
|
| 771 |
+
padding: 22px 22px 20px !important;
|
| 772 |
+
border-radius: 16px !important;
|
| 773 |
+
border: 1px solid hsl(var(--border)) !important;
|
| 774 |
+
background: hsl(var(--popover)) !important;
|
| 775 |
+
color: hsl(var(--foreground)) !important;
|
| 776 |
+
font-size: 17px !important;
|
| 777 |
+
font-weight: 700 !important;
|
| 778 |
+
line-height: 1.25 !important;
|
| 779 |
+
text-align: left !important;
|
| 780 |
+
white-space: normal !important;
|
| 781 |
+
cursor: pointer !important;
|
| 782 |
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06) !important;
|
| 783 |
+
transition: transform 0.13s ease-out, box-shadow 0.13s ease-out,
|
| 784 |
+
border-color 0.13s ease-out !important;
|
| 785 |
+
}
|
| 786 |
+
button[data-ac-entry]:hover {
|
| 787 |
+
transform: translateY(-2px) !important;
|
| 788 |
+
border-color: hsl(var(--primary)) !important;
|
| 789 |
+
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.16) !important;
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
/* Icon chip at the top of each card (monochrome, product-style). */
|
| 793 |
+
button[data-ac-entry]::before {
|
| 794 |
+
display: inline-flex;
|
| 795 |
+
align-items: center;
|
| 796 |
+
justify-content: center;
|
| 797 |
+
width: 42px;
|
| 798 |
+
height: 42px;
|
| 799 |
+
border-radius: 11px;
|
| 800 |
+
font-size: 22px;
|
| 801 |
+
font-weight: 700;
|
| 802 |
+
line-height: 1;
|
| 803 |
+
color: hsl(var(--primary));
|
| 804 |
+
background: color-mix(in oklab, hsl(var(--primary)) 14%, transparent);
|
| 805 |
+
}
|
| 806 |
+
button[data-ac-entry="create"]::before { content: "+"; }
|
| 807 |
+
button[data-ac-entry="validate"]::before { content: "\2713"; } /* ✓ */
|
| 808 |
+
|
| 809 |
+
/* Description line under the title. */
|
| 810 |
+
button[data-ac-entry]::after {
|
| 811 |
+
font-size: 13px;
|
| 812 |
+
font-weight: 400;
|
| 813 |
+
line-height: 1.45;
|
| 814 |
+
color: hsl(var(--muted-foreground));
|
| 815 |
+
white-space: normal;
|
| 816 |
+
}
|
| 817 |
+
button[data-ac-entry="create"]::after {
|
| 818 |
+
content: "Design a new competition bundle from a one-line idea or a PDF proposal.";
|
| 819 |
+
}
|
| 820 |
+
button[data-ac-entry="validate"]::after {
|
| 821 |
+
content: "Check an existing bundle against the pre-launch validation criteria.";
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
|
| 825 |
+
/* ============================================================
|
| 826 |
+
* Codabench rebrand — blue accents + light-blue tints to match
|
| 827 |
+
* codabench.org (brand blue #06649e, link #4183c4, tint #f2faff).
|
| 828 |
+
* Overrides Chainlit's shadcn theme variables. login.css loads after
|
| 829 |
+
* Chainlit's bundle, so these win in both light and dark.
|
| 830 |
+
* ============================================================ */
|
| 831 |
+
/* `:root:not(.dark)` (light) and `html.dark` are deliberately MORE specific
|
| 832 |
+
* than Chainlit's own `:root` / `.dark` so these win the cascade tie. */
|
| 833 |
+
:root:not(.dark) {
|
| 834 |
+
--primary: 203 88% 34%; /* Codabench blue */
|
| 835 |
+
--primary-foreground: 0 0% 100%;
|
| 836 |
+
--ring: 203 88% 34%;
|
| 837 |
+
--accent: 205 80% 93%; /* light-blue tint */
|
| 838 |
+
--accent-foreground: 203 80% 26%;
|
| 839 |
+
--secondary: 205 70% 95%;
|
| 840 |
+
--secondary-foreground: 203 70% 26%;
|
| 841 |
+
--background: 208 45% 98.5%; /* faint light-blue page tint */
|
| 842 |
+
}
|
| 843 |
+
html.dark {
|
| 844 |
+
--primary: 203 85% 56%; /* brighter blue on dark */
|
| 845 |
+
--primary-foreground: 0 0% 100%;
|
| 846 |
+
--ring: 203 85% 56%;
|
| 847 |
+
--accent: 203 42% 22%; /* deep blue-tinted accent */
|
| 848 |
+
--accent-foreground: 0 0% 96%;
|
| 849 |
+
--secondary: 203 28% 20%;
|
| 850 |
+
--secondary-foreground: 0 0% 96%;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
|
| 854 |
+
/* ============================================================
|
| 855 |
+
* Persistent cost / context widget (chat.js) — bottom-left,
|
| 856 |
+
* always-visible session spend + context use with a budget bar.
|
| 857 |
+
* ============================================================ */
|
| 858 |
+
#ac-cost-widget {
|
| 859 |
+
position: fixed;
|
| 860 |
+
left: 14px;
|
| 861 |
+
bottom: 14px;
|
| 862 |
+
z-index: 9000;
|
| 863 |
+
min-width: 152px;
|
| 864 |
+
padding: 8px 11px;
|
| 865 |
+
border-radius: 10px;
|
| 866 |
+
background: hsl(var(--popover));
|
| 867 |
+
border: 1px solid hsl(var(--border));
|
| 868 |
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.14);
|
| 869 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
| 870 |
+
Roboto, Helvetica, Arial, sans-serif;
|
| 871 |
+
font-size: 12px;
|
| 872 |
+
color: hsl(var(--foreground));
|
| 873 |
+
user-select: none;
|
| 874 |
+
pointer-events: none; /* informational; never blocks clicks */
|
| 875 |
+
}
|
| 876 |
+
.ac-cost-top {
|
| 877 |
+
display: flex;
|
| 878 |
+
align-items: center;
|
| 879 |
+
gap: 8px;
|
| 880 |
+
}
|
| 881 |
+
.ac-cost-budget { font-weight: 600; white-space: nowrap; }
|
| 882 |
+
.ac-cost-bar {
|
| 883 |
+
flex: 1;
|
| 884 |
+
height: 5px;
|
| 885 |
+
min-width: 36px;
|
| 886 |
+
border-radius: 999px;
|
| 887 |
+
background: hsl(var(--accent));
|
| 888 |
+
overflow: hidden;
|
| 889 |
+
}
|
| 890 |
+
.ac-cost-bar > i {
|
| 891 |
+
display: block;
|
| 892 |
+
height: 100%;
|
| 893 |
+
width: 0%;
|
| 894 |
+
background: hsl(var(--primary));
|
| 895 |
+
transition: width 0.4s ease-out, background 0.3s ease-out;
|
| 896 |
+
}
|
| 897 |
+
.ac-cost-ctx {
|
| 898 |
+
margin-top: 4px;
|
| 899 |
+
font-size: 11px;
|
| 900 |
+
color: hsl(var(--muted-foreground));
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
|
| 904 |
+
/* ============================================================
|
| 905 |
+
* Phase-pill custom tooltip + clickable "advance" pill (chat.js).
|
| 906 |
+
* ============================================================ */
|
| 907 |
+
#ac-pill-tip {
|
| 908 |
+
position: fixed;
|
| 909 |
+
z-index: 100002;
|
| 910 |
+
max-width: 280px;
|
| 911 |
+
padding: 8px 11px;
|
| 912 |
+
border-radius: 8px;
|
| 913 |
+
background: hsl(var(--popover));
|
| 914 |
+
color: hsl(var(--popover-foreground));
|
| 915 |
+
border: 1px solid hsl(var(--border));
|
| 916 |
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
|
| 917 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
| 918 |
+
Roboto, Helvetica, Arial, sans-serif;
|
| 919 |
+
font-size: 12px;
|
| 920 |
+
line-height: 1.45;
|
| 921 |
+
white-space: pre-line; /* render the \n\n in the tip text */
|
| 922 |
+
pointer-events: none;
|
| 923 |
+
}
|
| 924 |
+
#ac-pill-tip[hidden] { display: none; }
|
| 925 |
+
|
| 926 |
+
/* The next-phase pill, once it can be entered: blue, pulsing, clickable. */
|
| 927 |
+
.ac-pp-advance {
|
| 928 |
+
cursor: pointer !important;
|
| 929 |
+
color: #fff !important;
|
| 930 |
+
background: hsl(var(--primary)) !important;
|
| 931 |
+
border-color: hsl(var(--primary)) !important;
|
| 932 |
+
font-weight: 700 !important;
|
| 933 |
+
animation: ac-pp-advance-pulse 1.8s ease-in-out infinite;
|
| 934 |
+
}
|
| 935 |
+
.ac-pp-advance:hover { filter: brightness(1.12); }
|
| 936 |
+
@keyframes ac-pp-advance-pulse {
|
| 937 |
+
0%, 100% { box-shadow: 0 0 0 0 hsl(var(--primary) / 0.55); }
|
| 938 |
+
70% { box-shadow: 0 0 0 6px hsl(var(--primary) / 0); }
|
| 939 |
+
}
|
|
|
|
@@ -277,17 +277,21 @@ class SessionManager:
|
|
| 277 |
@staticmethod
|
| 278 |
async def _ask_entry_mode() -> str:
|
| 279 |
"""Ask the user, upfront, which path they want. Returns 'create'|'validate'."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
res = await cl.AskActionMessage(
|
| 281 |
content=(
|
| 282 |
-
"##
|
| 283 |
-
"
|
| 284 |
-
"
|
| 285 |
),
|
| 286 |
actions=[
|
| 287 |
cl.Action(name="entry_mode", payload={"mode": "create"},
|
| 288 |
-
label="
|
| 289 |
cl.Action(name="entry_mode", payload={"mode": "validate"},
|
| 290 |
-
label="
|
| 291 |
],
|
| 292 |
timeout=900,
|
| 293 |
).send()
|
|
@@ -350,23 +354,55 @@ class SessionManager:
|
|
| 350 |
content=f"_Model switched to **{label}** (`{model}`)._",
|
| 351 |
).send()
|
| 352 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
@staticmethod
|
| 354 |
async def _send_create_greeting(session_id: str) -> None:
|
| 355 |
"""Option A greeting. Must contain the READY_PHRASE for the init-gate."""
|
|
|
|
| 356 |
await cl.Message(
|
| 357 |
content=(
|
| 358 |
-
"#
|
| 359 |
-
"Tell me a competition idea — a sentence is enough
|
| 360 |
-
"
|
| 361 |
-
"
|
| 362 |
-
"fill in the gaps.\n\n"
|
| 363 |
-
"
|
| 364 |
-
"
|
| 365 |
-
"
|
| 366 |
-
"
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
),
|
| 371 |
author="autocodabench",
|
| 372 |
).send()
|
|
@@ -374,18 +410,29 @@ class SessionManager:
|
|
| 374 |
@staticmethod
|
| 375 |
async def _send_validate_greeting(session_id: str) -> None:
|
| 376 |
"""Option B greeting. Contains the attach-mode READY_PHRASE."""
|
|
|
|
| 377 |
await cl.Message(
|
| 378 |
content=(
|
| 379 |
-
|
| 380 |
-
"**Attach your bundle `.zip`** below and press send. I'll run "
|
| 381 |
-
"
|
| 382 |
-
"
|
| 383 |
-
"
|
| 384 |
-
"
|
| 385 |
-
"
|
| 386 |
-
"
|
| 387 |
-
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
),
|
| 390 |
author="autocodabench",
|
| 391 |
).send()
|
|
|
|
| 277 |
@staticmethod
|
| 278 |
async def _ask_entry_mode() -> str:
|
| 279 |
"""Ask the user, upfront, which path they want. Returns 'create'|'validate'."""
|
| 280 |
+
# Labels are intentionally short, clean titles — the per-card
|
| 281 |
+
# description line + icon are added by CSS (login.css, keyed off the
|
| 282 |
+
# data-ac-entry tag chat.js sets), so the landing reads like a product
|
| 283 |
+
# chooser rather than two emoji chat buttons.
|
| 284 |
res = await cl.AskActionMessage(
|
| 285 |
content=(
|
| 286 |
+
"## AutoCodabench\n\n"
|
| 287 |
+
"Choose how you'd like to start. You can switch any time "
|
| 288 |
+
"with **New Chat**."
|
| 289 |
),
|
| 290 |
actions=[
|
| 291 |
cl.Action(name="entry_mode", payload={"mode": "create"},
|
| 292 |
+
label="Create from scratch"),
|
| 293 |
cl.Action(name="entry_mode", payload={"mode": "validate"},
|
| 294 |
+
label="Validate a bundle"),
|
| 295 |
],
|
| 296 |
timeout=900,
|
| 297 |
).send()
|
|
|
|
| 354 |
content=f"_Model switched to **{label}** (`{model}`)._",
|
| 355 |
).send()
|
| 356 |
|
| 357 |
+
@staticmethod
|
| 358 |
+
def _meta_footer(pairs: list[tuple[str, str]]) -> str:
|
| 359 |
+
"""A clean metadata chip row (session / model / budget …).
|
| 360 |
+
|
| 361 |
+
Rendered as inline-styled HTML — config enables unsafe_allow_html, and
|
| 362 |
+
inline styles (unlike CSS classes) survive Chainlit's HTML sanitiser.
|
| 363 |
+
Colours use the app's theme variables so it tracks light/dark.
|
| 364 |
+
"""
|
| 365 |
+
chip = (
|
| 366 |
+
'<span style="display:inline-flex;align-items:center;gap:6px;'
|
| 367 |
+
'padding:3px 10px;border-radius:999px;background:hsl(var(--accent))">'
|
| 368 |
+
'<b style="font-size:10px;font-weight:600;text-transform:uppercase;'
|
| 369 |
+
'letter-spacing:.4px;opacity:.7">{k}</b>'
|
| 370 |
+
'<code style="font-family:ui-monospace,SFMono-Regular,monospace;'
|
| 371 |
+
'background:none;color:hsl(var(--foreground))">{v}</code></span>'
|
| 372 |
+
)
|
| 373 |
+
chips = "".join(chip.format(k=k, v=v) for k, v in pairs)
|
| 374 |
+
return (
|
| 375 |
+
'<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:16px;'
|
| 376 |
+
'padding-top:12px;border-top:1px solid hsl(var(--border));'
|
| 377 |
+
'font-size:12px">' + chips + "</div>"
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
@staticmethod
|
| 381 |
async def _send_create_greeting(session_id: str) -> None:
|
| 382 |
"""Option A greeting. Must contain the READY_PHRASE for the init-gate."""
|
| 383 |
+
model = cl.user_session.get("model") or DEFAULT_MODEL
|
| 384 |
await cl.Message(
|
| 385 |
content=(
|
| 386 |
+
"## Create from scratch\n\n"
|
| 387 |
+
"Tell me a competition idea — a sentence is enough. I'll explore "
|
| 388 |
+
"the design space with you, cite the relevant literature, and "
|
| 389 |
+
"turn it into a plan. You can also drop a PDF or markdown design "
|
| 390 |
+
"doc and I'll fill in the gaps.\n\n"
|
| 391 |
+
"**Not sure where to start? Try one of these:**\n"
|
| 392 |
+
"- *A fair chest-X-ray pneumonia challenge, scored by balanced "
|
| 393 |
+
"accuracy with a penalty for performance gaps across age groups.*\n"
|
| 394 |
+
"- *Forecast next-day household energy use from smart-meter "
|
| 395 |
+
"history, ranked by MASE.*\n"
|
| 396 |
+
"- *Low-resource Swahili→English machine translation, evaluated "
|
| 397 |
+
"with chrF.*\n\n"
|
| 398 |
+
"When the plan is ready, a **▶ Proceed to Phase 2** button "
|
| 399 |
+
"appears. The bar above tracks your progress; switch models "
|
| 400 |
+
"anytime from **⚙ settings** by the message box.\n\n"
|
| 401 |
+
+ SessionManager._meta_footer([
|
| 402 |
+
("session", session_id),
|
| 403 |
+
("model", model),
|
| 404 |
+
("budget", f"${MAX_USD_PER_SESSION:.2f}"),
|
| 405 |
+
])
|
| 406 |
),
|
| 407 |
author="autocodabench",
|
| 408 |
).send()
|
|
|
|
| 410 |
@staticmethod
|
| 411 |
async def _send_validate_greeting(session_id: str) -> None:
|
| 412 |
"""Option B greeting. Contains the attach-mode READY_PHRASE."""
|
| 413 |
+
model = cl.user_session.get("model") or DEFAULT_MODEL
|
| 414 |
await cl.Message(
|
| 415 |
content=(
|
| 416 |
+
"## Validate a bundle\n\n"
|
| 417 |
+
"**Attach your bundle `.zip`** below and press send. I'll run the "
|
| 418 |
+
"autocodabench checks against it — including a Docker execution "
|
| 419 |
+
"of the baseline (this can take a few minutes and may pull the "
|
| 420 |
+
"bundle's `docker_image`) — then write you a report.\n\n"
|
| 421 |
+
"_Typing stays disabled until validation runs — just attach the "
|
| 422 |
+
"file and send._\n\n"
|
| 423 |
+
"**Don't have a bundle handy?** Download this small example "
|
| 424 |
+
"competition, then attach it above to see validation in action:\n\n"
|
| 425 |
+
'<a href="/public/examples/example-bundle-survival.zip" download '
|
| 426 |
+
'target="_blank" style="display:inline-flex;align-items:center;'
|
| 427 |
+
'gap:8px;padding:9px 16px;border-radius:10px;'
|
| 428 |
+
'background:hsl(var(--primary));color:#fff;font-weight:600;'
|
| 429 |
+
'font-size:13px;text-decoration:none">⬇ Download example '
|
| 430 |
+
'bundle <span style="opacity:.75;font-weight:400">survival '
|
| 431 |
+
'· 80 KB</span></a>\n\n'
|
| 432 |
+
+ SessionManager._meta_footer([
|
| 433 |
+
("session", session_id),
|
| 434 |
+
("judge model", model),
|
| 435 |
+
])
|
| 436 |
),
|
| 437 |
author="autocodabench",
|
| 438 |
).send()
|
|
@@ -236,20 +236,47 @@ class TurnView:
|
|
| 236 |
extra = f" · {self._status}" if self._status else ""
|
| 237 |
return f"_Composing… {_blob(self._frame)} · {elapsed}s{extra}_"
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
def _content(self) -> str:
|
| 240 |
-
# Consecutive log lines pack into one tight block (markdown hard breaks
|
| 241 |
-
#
|
|
|
|
| 242 |
blocks: list[str] = []
|
| 243 |
run: list[str] = []
|
| 244 |
for it in self._items:
|
| 245 |
if it["kind"] == "log":
|
| 246 |
run.append(it["text"])
|
| 247 |
else:
|
| 248 |
-
|
| 249 |
-
|
|
|
|
| 250 |
blocks.append(it["text"])
|
| 251 |
-
|
| 252 |
-
|
|
|
|
| 253 |
tail = self._tail()
|
| 254 |
if tail:
|
| 255 |
blocks.append(tail)
|
|
@@ -455,25 +482,34 @@ async def run_agent_turn(
|
|
| 455 |
cl.user_session.set("cum_cost_usd", cum)
|
| 456 |
|
| 457 |
usage = getattr(message, "usage", None) or {}
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
log.info("[turn] ResultMessage cost=$%.4f cum=$%.4f in_tok=%d out_tok=%d", cost, cum, in_tok, out_tok)
|
| 465 |
if in_tok:
|
| 466 |
cl.user_session.set("last_input_tokens", in_tok)
|
| 467 |
if out_tok:
|
| 468 |
cl.user_session.set("last_output_tokens", out_tok)
|
| 469 |
|
|
|
|
|
|
|
|
|
|
| 470 |
if cost or in_tok:
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
)
|
| 477 |
|
| 478 |
user = cl.user_session.get("user")
|
| 479 |
CostLog.append(
|
|
|
|
| 236 |
extra = f" · {self._status}" if self._status else ""
|
| 237 |
return f"_Composing… {_blob(self._frame)} · {elapsed}s{extra}_"
|
| 238 |
|
| 239 |
+
def _flush_log_run(self, run: list[str]) -> str:
|
| 240 |
+
"""Render a run of tool/step log lines.
|
| 241 |
+
|
| 242 |
+
While the turn is WORKING the steps stream live as plain lines. Once the
|
| 243 |
+
turn is done they collapse into a single hide/unhide ``<details>`` so the
|
| 244 |
+
final answer is what stands out — the user can expand to see every step.
|
| 245 |
+
(Inline styles, not classes, so they survive Chainlit's HTML sanitiser.)
|
| 246 |
+
"""
|
| 247 |
+
if not run:
|
| 248 |
+
return ""
|
| 249 |
+
if self._working:
|
| 250 |
+
return " \n".join(run)
|
| 251 |
+
n = len(run)
|
| 252 |
+
inner = "<br>".join(run)
|
| 253 |
+
return (
|
| 254 |
+
'<details style="margin:2px 0"><summary style="cursor:pointer;'
|
| 255 |
+
'color:hsl(var(--muted-foreground));font-size:13px;'
|
| 256 |
+
'user-select:none">🔧 '
|
| 257 |
+
f'{n} step{"s" if n != 1 else ""} · show / hide</summary>'
|
| 258 |
+
'<div style="font-family:ui-monospace,SFMono-Regular,monospace;'
|
| 259 |
+
'font-size:12px;color:hsl(var(--muted-foreground));'
|
| 260 |
+
f'margin:6px 0 4px;line-height:1.7">{inner}</div></details>'
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
def _content(self) -> str:
|
| 264 |
+
# Consecutive log lines pack into one tight block (markdown hard breaks
|
| 265 |
+
# while live; a collapsed <details> once done); narration/footer blocks
|
| 266 |
+
# stand alone so prose and tables render right.
|
| 267 |
blocks: list[str] = []
|
| 268 |
run: list[str] = []
|
| 269 |
for it in self._items:
|
| 270 |
if it["kind"] == "log":
|
| 271 |
run.append(it["text"])
|
| 272 |
else:
|
| 273 |
+
block = self._flush_log_run(run); run = []
|
| 274 |
+
if block:
|
| 275 |
+
blocks.append(block)
|
| 276 |
blocks.append(it["text"])
|
| 277 |
+
block = self._flush_log_run(run)
|
| 278 |
+
if block:
|
| 279 |
+
blocks.append(block)
|
| 280 |
tail = self._tail()
|
| 281 |
if tail:
|
| 282 |
blocks.append(tail)
|
|
|
|
| 482 |
cl.user_session.set("cum_cost_usd", cum)
|
| 483 |
|
| 484 |
usage = getattr(message, "usage", None) or {}
|
| 485 |
+
|
| 486 |
+
def _u(key: str) -> int:
|
| 487 |
+
v = usage.get(key) if isinstance(usage, dict) else getattr(usage, key, 0)
|
| 488 |
+
return int(v or 0)
|
| 489 |
+
|
| 490 |
+
# The context gauge needs the FULL prompt size. With prompt
|
| 491 |
+
# caching, `input_tokens` alone is just the handful of uncached
|
| 492 |
+
# tokens (e.g. 12), so it read ~0% even on a long conversation —
|
| 493 |
+
# add the cached portion (read + creation) back in.
|
| 494 |
+
in_tok = (_u("input_tokens")
|
| 495 |
+
+ _u("cache_read_input_tokens")
|
| 496 |
+
+ _u("cache_creation_input_tokens"))
|
| 497 |
+
out_tok = _u("output_tokens")
|
| 498 |
log.info("[turn] ResultMessage cost=$%.4f cum=$%.4f in_tok=%d out_tok=%d", cost, cum, in_tok, out_tok)
|
| 499 |
if in_tok:
|
| 500 |
cl.user_session.set("last_input_tokens", in_tok)
|
| 501 |
if out_tok:
|
| 502 |
cl.user_session.set("last_output_tokens", out_tok)
|
| 503 |
|
| 504 |
+
# Refresh phase_state.json so the always-visible cost/context
|
| 505 |
+
# widget (chat.js, fed by the phase_state poll) shows live
|
| 506 |
+
# numbers. The per-turn line is no longer printed inline.
|
| 507 |
if cost or in_tok:
|
| 508 |
+
try:
|
| 509 |
+
from phase_manager import PhaseManager
|
| 510 |
+
PhaseManager.write_state(run_dir)
|
| 511 |
+
except Exception:
|
| 512 |
+
log.debug("[turn] phase_state cost refresh failed", exc_info=True)
|
|
|
|
| 513 |
|
| 514 |
user = cl.user_session.get("user")
|
| 515 |
CostLog.append(
|