submit+app: toast-based feedback (validating / queued / refreshed)
Browse filesBundle 1+2 C9. Replaces the static markdown reply on the Submit
tab with gr.Info / gr.Error toasts, and adds a refresh toast on the
manual Refresh button (the Timer auto-refresh stays silent - a
toast every 10s would be noise).
submit.py:
- handle_submit no longer returns a markdown string. It side-
effects: gr.Info("Validating submission...") at the top of the
validation stage, gr.Info("Submission <id> queued ...") after
the row + zip are on the Hub and the worker has been spawned.
Every rejection path raises gr.Error so the user gets a red
toast and the handler aborts; the outer try/finally still runs
cleanup. Internal _ValidationError + _HubWriteError sentinels
preserved.
- _validate_form returns a plain message (no `**Error:**` markdown
wrap; the caller wraps into gr.Error).
- All four rejection paths covered:
1. Form (no zip attached).
2. Validation gate (extract / meta / fixtures / steps).
3. Dedup gate (sha256 match on existing row).
4. Hub write failure.
app.py:
- Submit tab drops `submit_out = gr.Markdown()`; submit_btn.click
wires straight to handle_submit with no outputs.
- New `_refresh_leaderboard_with_toast()` wraps `load_leaderboard_
split` for the manual Refresh button click; the existing Timer
binding still calls load_leaderboard_split directly so it
doesn't toast.
Verification (autonomous):
- 22/22 unit tests still green.
- Local boot probe: GET /config carries no leftover markdown
strings ("**Submission rejected.**", "**Queued.**", "**Error:**",
"please attach a submission zip"). All earlier-bundle features
remain (Citation + Validation Guidelines accordions, Download
CSV button, downloadbutton component).
Post-push live probe runs next.
|
@@ -147,6 +147,17 @@ def _build_report_iframe(html_bytes: bytes) -> str:
|
|
| 147 |
)
|
| 148 |
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
def _format_detail_and_report(
|
| 151 |
df: pd.DataFrame | None, evt: gr.SelectData,
|
| 152 |
) -> tuple[str, str]:
|
|
@@ -293,7 +304,7 @@ with gr.Blocks(title="CADGenBench Leaderboard", theme=gr.themes.Soft()) as block
|
|
| 293 |
label="Download CSV", size="sm",
|
| 294 |
)
|
| 295 |
refresh_btn.click(
|
| 296 |
-
fn=
|
| 297 |
outputs=[validated_view, unvalidated_view],
|
| 298 |
)
|
| 299 |
download_btn.click(fn=build_combined_csv, outputs=download_btn)
|
|
@@ -350,12 +361,9 @@ to publish the resulting row on the public leaderboard.
|
|
| 350 |
)
|
| 351 |
zip_in = gr.File(label="Submission ZIP", file_types=[".zip"])
|
| 352 |
submit_btn = gr.Button("Submit", variant="primary")
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
inputs=[zip_in],
|
| 357 |
-
outputs=submit_out,
|
| 358 |
-
)
|
| 359 |
|
| 360 |
with gr.Tab("About"):
|
| 361 |
gr.Markdown(ABOUT_MD)
|
|
|
|
| 147 |
)
|
| 148 |
|
| 149 |
|
| 150 |
+
def _refresh_leaderboard_with_toast():
|
| 151 |
+
"""Manual Refresh button handler: toast + fresh DataFrames.
|
| 152 |
+
|
| 153 |
+
The Timer auto-refresh wires straight to ``load_leaderboard_split``
|
| 154 |
+
so it stays silent (a toast every 10s would be noise). Only the
|
| 155 |
+
explicit click goes through this wrapper.
|
| 156 |
+
"""
|
| 157 |
+
gr.Info("Leaderboard refreshed.")
|
| 158 |
+
return load_leaderboard_split()
|
| 159 |
+
|
| 160 |
+
|
| 161 |
def _format_detail_and_report(
|
| 162 |
df: pd.DataFrame | None, evt: gr.SelectData,
|
| 163 |
) -> tuple[str, str]:
|
|
|
|
| 304 |
label="Download CSV", size="sm",
|
| 305 |
)
|
| 306 |
refresh_btn.click(
|
| 307 |
+
fn=_refresh_leaderboard_with_toast,
|
| 308 |
outputs=[validated_view, unvalidated_view],
|
| 309 |
)
|
| 310 |
download_btn.click(fn=build_combined_csv, outputs=download_btn)
|
|
|
|
| 361 |
)
|
| 362 |
zip_in = gr.File(label="Submission ZIP", file_types=[".zip"])
|
| 363 |
submit_btn = gr.Button("Submit", variant="primary")
|
| 364 |
+
# No static markdown output: handle_submit surfaces every
|
| 365 |
+
# status update via gr.Info / gr.Error toasts.
|
| 366 |
+
submit_btn.click(fn=handle_submit, inputs=[zip_in])
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
with gr.Tab("About"):
|
| 369 |
gr.Markdown(ABOUT_MD)
|
|
@@ -77,6 +77,7 @@ from pathlib import Path
|
|
| 77 |
from typing import Any
|
| 78 |
|
| 79 |
import cadgenbench
|
|
|
|
| 80 |
from cadgenbench.common.paths import data_inputs_dir
|
| 81 |
from cadgenbench.common.validity import parse_step
|
| 82 |
from huggingface_hub import HfApi
|
|
@@ -130,21 +131,28 @@ class _HubWriteError(Exception):
|
|
| 130 |
"""Raised when a Hub upload fails after validation succeeded."""
|
| 131 |
|
| 132 |
|
| 133 |
-
def handle_submit(zip_file) ->
|
| 134 |
-
"""Validate a submission upload
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
"""
|
| 145 |
form_err = _validate_form(zip_file)
|
| 146 |
if form_err is not None:
|
| 147 |
-
|
| 148 |
|
| 149 |
zip_path = Path(zip_file.name)
|
| 150 |
|
|
@@ -156,13 +164,14 @@ def handle_submit(zip_file) -> str:
|
|
| 156 |
run_dir = tmp / "run"
|
| 157 |
run_dir.mkdir()
|
| 158 |
try:
|
|
|
|
| 159 |
try:
|
| 160 |
_extract_zip(zip_path, run_dir)
|
| 161 |
meta = _load_and_validate_meta(run_dir)
|
| 162 |
fixture_names = _validate_fixture_set(run_dir)
|
| 163 |
_validate_steps_parseable(run_dir, fixture_names)
|
| 164 |
except _ValidationError as e:
|
| 165 |
-
|
| 166 |
|
| 167 |
# Dedup gate: hash the raw zip bytes and reject if an existing
|
| 168 |
# row carries the same hash. Runs after validation so a clearly
|
|
@@ -170,10 +179,10 @@ def handle_submit(zip_file) -> str:
|
|
| 170 |
zip_sha256 = _compute_sha256(zip_path)
|
| 171 |
existing_id = _find_existing_submission_by_sha256(zip_sha256)
|
| 172 |
if existing_id is not None:
|
| 173 |
-
|
| 174 |
-
f"
|
| 175 |
-
f"
|
| 176 |
-
f"
|
| 177 |
)
|
| 178 |
|
| 179 |
submission_id = _mint_submission_id(
|
|
@@ -186,27 +195,30 @@ def handle_submit(zip_file) -> str:
|
|
| 186 |
)
|
| 187 |
_append_pending_row(row)
|
| 188 |
except _HubWriteError as e:
|
| 189 |
-
|
| 190 |
|
| 191 |
_spawn_worker(submission_id, tmp, run_dir)
|
| 192 |
tmp = None # ownership transferred; skip cleanup below
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
finally:
|
| 194 |
if tmp is not None:
|
| 195 |
shutil.rmtree(tmp, ignore_errors=True)
|
| 196 |
|
| 197 |
-
return (
|
| 198 |
-
f"**Queued.** Submission `{submission_id}` has been accepted "
|
| 199 |
-
f"(submitter: `{meta['submitter_name']}`, system: "
|
| 200 |
-
f"`{meta['submission_name']}`, {len(fixture_names)} fixtures). "
|
| 201 |
-
f"Evaluation typically takes 2-5 minutes on this Space's "
|
| 202 |
-
f"`cpu-upgrade` tier; the row flips to `completed` with score "
|
| 203 |
-
f"columns populated when the worker finishes."
|
| 204 |
-
)
|
| 205 |
-
|
| 206 |
|
| 207 |
def _validate_form(zip_file) -> str | None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
if zip_file is None:
|
| 209 |
-
return "
|
| 210 |
return None
|
| 211 |
|
| 212 |
|
|
|
|
| 77 |
from typing import Any
|
| 78 |
|
| 79 |
import cadgenbench
|
| 80 |
+
import gradio as gr
|
| 81 |
from cadgenbench.common.paths import data_inputs_dir
|
| 82 |
from cadgenbench.common.validity import parse_step
|
| 83 |
from huggingface_hub import HfApi
|
|
|
|
| 131 |
"""Raised when a Hub upload fails after validation succeeded."""
|
| 132 |
|
| 133 |
|
| 134 |
+
def handle_submit(zip_file) -> None:
|
| 135 |
+
"""Validate a submission upload; surface progress + outcome via toasts.
|
| 136 |
|
| 137 |
+
Side-effect-only (returns ``None``): every status update lands in
|
| 138 |
+
a ``gr.Info`` / ``gr.Error`` toast instead of a static markdown
|
| 139 |
+
output below the button. Rejection paths raise ``gr.Error``,
|
| 140 |
+
which Gradio surfaces as a red toast and aborts the handler; the
|
| 141 |
+
outer ``try/finally`` still runs to clean up the temp dir.
|
| 142 |
+
|
| 143 |
+
Toast sequence on the happy path:
|
| 144 |
+
1. ``gr.Info("Validating submission...")`` once the form-level
|
| 145 |
+
check is past.
|
| 146 |
+
2. ``gr.Info("Submission <id> queued for evaluation...")`` once
|
| 147 |
+
the pending row + zip are on the Hub and the worker has
|
| 148 |
+
been spawned.
|
| 149 |
+
|
| 150 |
+
On rejection (form-level, validation gate, dedup, or Hub write),
|
| 151 |
+
a single ``gr.Error`` toast carries the message; no second toast.
|
| 152 |
"""
|
| 153 |
form_err = _validate_form(zip_file)
|
| 154 |
if form_err is not None:
|
| 155 |
+
raise gr.Error(form_err)
|
| 156 |
|
| 157 |
zip_path = Path(zip_file.name)
|
| 158 |
|
|
|
|
| 164 |
run_dir = tmp / "run"
|
| 165 |
run_dir.mkdir()
|
| 166 |
try:
|
| 167 |
+
gr.Info("Validating submission...")
|
| 168 |
try:
|
| 169 |
_extract_zip(zip_path, run_dir)
|
| 170 |
meta = _load_and_validate_meta(run_dir)
|
| 171 |
fixture_names = _validate_fixture_set(run_dir)
|
| 172 |
_validate_steps_parseable(run_dir, fixture_names)
|
| 173 |
except _ValidationError as e:
|
| 174 |
+
raise gr.Error(f"Submission rejected: {e}")
|
| 175 |
|
| 176 |
# Dedup gate: hash the raw zip bytes and reject if an existing
|
| 177 |
# row carries the same hash. Runs after validation so a clearly
|
|
|
|
| 179 |
zip_sha256 = _compute_sha256(zip_path)
|
| 180 |
existing_id = _find_existing_submission_by_sha256(zip_sha256)
|
| 181 |
if existing_id is not None:
|
| 182 |
+
raise gr.Error(
|
| 183 |
+
f"This zip's contents are identical to an existing "
|
| 184 |
+
f"submission ({existing_id}). Resubmit only after changing "
|
| 185 |
+
f"at least one byte of the upload."
|
| 186 |
)
|
| 187 |
|
| 188 |
submission_id = _mint_submission_id(
|
|
|
|
| 195 |
)
|
| 196 |
_append_pending_row(row)
|
| 197 |
except _HubWriteError as e:
|
| 198 |
+
raise gr.Error(f"Submission rejected: {e}")
|
| 199 |
|
| 200 |
_spawn_worker(submission_id, tmp, run_dir)
|
| 201 |
tmp = None # ownership transferred; skip cleanup below
|
| 202 |
+
gr.Info(
|
| 203 |
+
f"Submission {submission_id} queued for evaluation "
|
| 204 |
+
f"({len(fixture_names)} fixtures). Evaluation typically "
|
| 205 |
+
f"takes 2-5 minutes; the row flips to completed when the "
|
| 206 |
+
f"worker finishes."
|
| 207 |
+
)
|
| 208 |
finally:
|
| 209 |
if tmp is not None:
|
| 210 |
shutil.rmtree(tmp, ignore_errors=True)
|
| 211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
def _validate_form(zip_file) -> str | None:
|
| 214 |
+
"""Form-level check before any zip parsing.
|
| 215 |
+
|
| 216 |
+
Returns a plain-text rejection message (no markdown wrapping;
|
| 217 |
+
the caller wraps it into a ``gr.Error`` toast) or ``None`` when
|
| 218 |
+
the form is acceptable.
|
| 219 |
+
"""
|
| 220 |
if zip_file is None:
|
| 221 |
+
return "Please attach a submission zip."
|
| 222 |
return None
|
| 223 |
|
| 224 |
|