Michael Rabinovich commited on
Commit
6facf47
·
1 Parent(s): f585077

submit+app: toast-based feedback (validating / queued / refreshed)

Browse files

Bundle 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.

Files changed (2) hide show
  1. app.py +15 -7
  2. submit.py +39 -27
app.py CHANGED
@@ -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=load_leaderboard_split,
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
- submit_out = gr.Markdown()
354
- submit_btn.click(
355
- fn=handle_submit,
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)
submit.py CHANGED
@@ -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) -> str:
134
- """Validate a submission upload and return a markdown UI message.
135
 
136
- Returns one of:
137
- - An error string starting with ``**Error:**`` for form-level
138
- rejects (no zip attached).
139
- - A rejection string starting with ``**Submission rejected.**``
140
- for any of the deeper validation gates or Hub-write failures.
141
- - A success string starting with ``**Queued.**`` carrying the
142
- minted ``submission_id`` once the pending row + zip are on
143
- the Hub.
 
 
 
 
 
 
 
144
  """
145
  form_err = _validate_form(zip_file)
146
  if form_err is not None:
147
- return form_err
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
- return f"**Submission rejected.** {e}"
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
- return (
174
- f"**Submission rejected.** This zip's contents are identical "
175
- f"to an existing submission (`{existing_id}`). Resubmit only "
176
- f"after changing at least one byte of the upload."
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
- return f"**Submission rejected.** {e}"
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 "**Error:** please attach a submission zip."
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