From 27f06480931e22bc20e3f907690ef6471f80e8b3 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 16 May 2026 20:56:52 +0000 Subject: [PATCH] fix(text-cleaner): make all three download buttons actually fire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only "Download cleaned CSV" was working; "Download changes audit" and "Download config JSON" did nothing on click. The symptom is the classic Streamlit footgun for multiple ``st.download_button`` widgets in adjacent columns: without an explicit ``key`` argument the auto-derived widget IDs can collide, especially when one button is conditionally rendered, and only the first button in source order actually fires on click. Same goes for unstable ``data`` bytes recomputed inside the ``with col:`` block — the widget identity can drift between renders. Robustness pattern applied: - Compute all three byte buffers up front, outside the columns, so the ``data`` parameter is the same object across reruns. - Pass an explicit unique ``key`` ("textclean_dl_cleaned" / "textclean_dl_changes" / "textclean_dl_config") to each button. - Render the changes button unconditionally with ``disabled=True`` and a help tooltip when ``result.changes.empty`` — instead of hiding it. Layout stays steady and the empty case is self-explanatory. - ``use_container_width=True`` so the three buttons size identically inside their columns. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gui/pages/2_Text_Cleaner.py | 40 ++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/gui/pages/2_Text_Cleaner.py b/src/gui/pages/2_Text_Cleaner.py index 61ecf02..8b9db5f 100644 --- a/src/gui/pages/2_Text_Cleaner.py +++ b/src/gui/pages/2_Text_Cleaner.py @@ -328,35 +328,55 @@ else: # --------------------------------------------------------------------------- # Downloads # --------------------------------------------------------------------------- +# +# All three byte buffers are prepared up front (outside the columns) so +# each ``st.download_button`` sees stable ``data`` across reruns and an +# explicit ``key`` — without those, Streamlit auto-derived widget IDs +# can collide for multiple download_buttons in adjacent columns and +# only the first one actually fires on click. The empty-changes case +# now renders a disabled button (rather than vanishing) so the layout +# stays steady and the user understands why nothing's available. st.divider() stem = Path(st.session_state.get("textclean_input_name", "input")).stem +cleaned_bytes = result.cleaned_df.to_csv(index=False).encode("utf-8-sig") +changes_bytes = ( + result.changes.to_csv(index=False).encode("utf-8-sig") + if not result.changes.empty + else b"" +) +config_bytes = json.dumps(options.to_dict(), indent=2).encode("utf-8") + dl_a, dl_b, dl_c = st.columns(3) with dl_a: - cleaned_bytes = result.cleaned_df.to_csv(index=False).encode("utf-8-sig") st.download_button( "Download cleaned CSV", data=cleaned_bytes, file_name=f"{stem}_cleaned.csv", mime="text/csv", + key="textclean_dl_cleaned", + use_container_width=True, ) with dl_b: - if not result.changes.empty: - changes_bytes = result.changes.to_csv(index=False).encode("utf-8-sig") - st.download_button( - "Download changes audit", - data=changes_bytes, - file_name=f"{stem}_changes.csv", - mime="text/csv", - ) + st.download_button( + "Download changes audit", + data=changes_bytes, + file_name=f"{stem}_changes.csv", + mime="text/csv", + key="textclean_dl_changes", + disabled=result.changes.empty, + help="No changes to audit." if result.changes.empty else None, + use_container_width=True, + ) with dl_c: - config_bytes = json.dumps(options.to_dict(), indent=2).encode("utf-8") st.download_button( "Download config JSON", data=config_bytes, file_name="text_clean_config.json", mime="application/json", + key="textclean_dl_config", + use_container_width=True, ) st.divider()