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) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 19:57:01 +00:00
parent db5ec084da
commit fc6c22c6a7
2 changed files with 55 additions and 11 deletions

View File

@@ -286,15 +286,51 @@ st.caption(
"the rest before applying." "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 [] findings: list[Finding] = st.session_state.get("home_findings") or []
upload_name = st.session_state.get("home_uploaded_name") upload_name = st.session_state.get("home_uploaded_name")
if not upload_name: if not upload_name:
st.warning("No file uploaded. Go back to the home page and upload a CSV or Excel file first.") st.info(
if st.button("Back to home"): "Upload a CSV or Excel file to begin reviewing. The analyzer runs "
st.switch_page("app.py") "locally and your data never leaves this computer."
st.stop() )
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 -------------------------------------------------------- # ---- Encoding picker --------------------------------------------------------
# #

View File

@@ -156,16 +156,24 @@ class TestPipelineRunnerWorkflow:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestReviewWorkflow: class TestReviewWorkflow:
"""The Review page is the gate-fixer. Without an upload it shows a """The Review page is the gate-fixer. Without an upload it shows
'go back to home' message. With an upload it runs the analyzer and its own file uploader so the user can start the flow from this
shows findings.""" 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 = app_factory("0_Review")
app.run() app.run()
text = collected_text(app) text = collected_text(app)
# Page shows ``No file uploaded`` + ``Back to home``. # Page should invite the user to upload, not redirect home.
assert "No file uploaded" in text or "uploaded" in text.lower() 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( def test_with_upload_shows_review_content(
self, app_factory, small_csv_bytes, self, app_factory, small_csv_bytes,