From fc6c22c6a7cb96fedebe9fa1407f6ec178c246d0 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 May 2026 19:57:01 +0000 Subject: [PATCH] feat(review): inline file uploader instead of redirect home When a user lands on Review without an upload, show a file uploader on the page itself and auto-run the analyzer once a file is picked, rather than bouncing them to the home page with a "Back to home" button. Auto-analyze is the right default here: the user is already on the Review page, so they've implicitly committed to a scan. Stashing the bytes in the same session-state keys the home page uses keeps the rest of the flow (encoding picker, gate, tool pages) unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/pages/0_Review.py | 46 +++++++++++++++++++++++++++++++++---- tests/gui/test_workflows.py | 20 +++++++++++----- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/gui/pages/0_Review.py b/src/gui/pages/0_Review.py index 0d0fd5f..6fcec47 100644 --- a/src/gui/pages/0_Review.py +++ b/src/gui/pages/0_Review.py @@ -286,15 +286,51 @@ st.caption( "the rest before applying." ) -# Pre-flight: nothing to review without an upload. +# Pre-flight: if nothing has been uploaded yet, let the user upload +# directly from this page instead of bouncing them back to the home +# screen. Once a file is picked, we auto-run the analyzer (the user is +# already on the Review page — they've implicitly committed to a scan), +# stash the result, and rerun so the rest of the page picks it up. findings: list[Finding] = st.session_state.get("home_findings") or [] upload_name = st.session_state.get("home_uploaded_name") if not upload_name: - st.warning("No file uploaded. Go back to the home page and upload a CSV or Excel file first.") - if st.button("Back to home"): - st.switch_page("app.py") - st.stop() + st.info( + "Upload a CSV or Excel file to begin reviewing. The analyzer runs " + "locally and your data never leaves this computer." + ) + review_upload = st.file_uploader( + "Choose a file", + type=["csv", "tsv", "xlsx", "xls"], + key="review_upload", + help="Drag-and-drop or browse for a CSV, TSV, or Excel file.", + ) + if review_upload is None: + st.stop() + + # New file → stash bytes + size + name, drop any stale state, then + # run the analyzer. The rerun at the bottom lets the rest of this + # page render with the upload in place. + same_file = ( + st.session_state.get("home_uploaded_name") == review_upload.name + and st.session_state.get("home_uploaded_size") == review_upload.size + ) + if not same_file: + st.session_state["home_uploaded_name"] = review_upload.name + st.session_state["home_uploaded_size"] = review_upload.size + st.session_state["home_uploaded_bytes"] = review_upload.getvalue() + st.session_state.pop("home_findings", None) + st.session_state.pop("home_skipped", None) + st.session_state.pop("review_decisions", None) + st.session_state.pop("normalization_result", None) + st.session_state.pop("normalization_for", None) + st.session_state.pop("encoding_override", None) + + if st.session_state.get("home_findings") is None: + with st.spinner("Analyzing…"): + st.session_state["home_findings"] = _run_analysis_with_override(None) + st.session_state["home_skipped"] = False + st.rerun() # ---- Encoding picker -------------------------------------------------------- # diff --git a/tests/gui/test_workflows.py b/tests/gui/test_workflows.py index 77384c4..5a0f280 100644 --- a/tests/gui/test_workflows.py +++ b/tests/gui/test_workflows.py @@ -156,16 +156,24 @@ class TestPipelineRunnerWorkflow: # --------------------------------------------------------------------------- class TestReviewWorkflow: - """The Review page is the gate-fixer. Without an upload it shows a - 'go back to home' message. With an upload it runs the analyzer and - shows findings.""" + """The Review page is the gate-fixer. Without an upload it shows + its own file uploader so the user can start the flow from this + page directly. With an upload it runs the analyzer and shows + findings.""" - def test_no_upload_shows_back_to_home(self, app_factory): + def test_no_upload_shows_inline_uploader(self, app_factory): app = app_factory("0_Review") app.run() text = collected_text(app) - # Page shows ``No file uploaded`` + ``Back to home``. - assert "No file uploaded" in text or "uploaded" in text.lower() + # Page should invite the user to upload, not redirect home. + assert "Upload" in text or "Choose a file" in text, ( + f"Review page should expose an inline uploader; got:\n{text[:400]}" + ) + # The 'Back to home' button is gone — the page is self-contained now. + labels = [b.label for b in app.button] + assert not any("Back to home" in lbl for lbl in labels), ( + f"Back-to-home button should be removed; got buttons: {labels}" + ) def test_with_upload_shows_review_content( self, app_factory, small_csv_bytes,