From 558e186c66dee69a545f1baf8e5924ea998f86ca Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 14:40:47 -0800 Subject: [PATCH] feat: paste/drag-drop image component, remove server-side Playwright capture button --- app/components/paste_image.py | 31 +++++ app/components/paste_image_ui/index.html | 142 +++++++++++++++++++++++ app/feedback.py | 45 ++++--- 3 files changed, 194 insertions(+), 24 deletions(-) create mode 100644 app/components/paste_image.py create mode 100644 app/components/paste_image_ui/index.html diff --git a/app/components/paste_image.py b/app/components/paste_image.py new file mode 100644 index 0000000..9fdb46e --- /dev/null +++ b/app/components/paste_image.py @@ -0,0 +1,31 @@ +""" +Paste-from-clipboard / drag-and-drop image component. + +Uses st.components.v1.declare_component so JS can return image bytes to Python +(st.components.v1.html() is one-way only). No build step required — the +frontend is a single index.html file. +""" +from __future__ import annotations + +import base64 +from pathlib import Path + +import streamlit.components.v1 as components + +_FRONTEND = Path(__file__).parent / "paste_image_ui" + +_paste_image = components.declare_component("paste_image", path=str(_FRONTEND)) + + +def paste_image_component(key: str | None = None) -> bytes | None: + """ + Render the paste/drop zone. Returns PNG/JPEG bytes when an image is + pasted or dropped, or None if nothing has been submitted yet. + """ + result = _paste_image(key=key) + if result: + try: + return base64.b64decode(result) + except Exception: + return None + return None diff --git a/app/components/paste_image_ui/index.html b/app/components/paste_image_ui/index.html new file mode 100644 index 0000000..9fe83cb --- /dev/null +++ b/app/components/paste_image_ui/index.html @@ -0,0 +1,142 @@ + + + + + + + +
+ 📋 + Click here, then Ctrl+V to paste + or drag & drop an image file +
+
+ + + + diff --git a/app/feedback.py b/app/feedback.py index 1267e13..e4d0b51 100644 --- a/app/feedback.py +++ b/app/feedback.py @@ -35,8 +35,7 @@ def _feedback_dialog(page: str) -> None: """Two-step feedback dialog: form → consent/attachments → submit.""" from scripts.feedback_api import ( collect_context, collect_logs, collect_listings, - build_issue_body, create_forgejo_issue, - upload_attachment, screenshot_page, + build_issue_body, create_forgejo_issue, upload_attachment, ) from scripts.db import DEFAULT_DB @@ -104,29 +103,26 @@ def _feedback_dialog(page: str) -> None: # ── Screenshot ──────────────────────────────────────────────────────── st.divider() st.caption("**Screenshot** (optional)") - col_cap, col_up = st.columns(2) - with col_cap: - if st.button("📸 Capture current view"): - with st.spinner("Capturing page…"): - png = screenshot_page() - if png: - st.session_state.fb_screenshot = png - else: - st.warning( - "Playwright not available — install it with " - "`playwright install chromium`, or upload a screenshot instead." - ) + from app.components.paste_image import paste_image_component - with col_up: - uploaded = st.file_uploader( - "Upload screenshot", - type=["png", "jpg", "jpeg"], - label_visibility="collapsed", - key="fb_upload", - ) - if uploaded: - st.session_state.fb_screenshot = uploaded.read() + # Keyed so we can reset the component when the user removes the image + if "fb_paste_key" not in st.session_state: + st.session_state.fb_paste_key = 0 + + pasted = paste_image_component(key=f"fb_paste_{st.session_state.fb_paste_key}") + if pasted: + st.session_state.fb_screenshot = pasted + + st.caption("or upload a file:") + uploaded = st.file_uploader( + "Upload screenshot", + type=["png", "jpg", "jpeg"], + label_visibility="collapsed", + key="fb_upload", + ) + if uploaded: + st.session_state.fb_screenshot = uploaded.read() if st.session_state.get("fb_screenshot"): st.image( @@ -136,6 +132,7 @@ def _feedback_dialog(page: str) -> None: ) if st.button("🗑 Remove screenshot"): st.session_state.pop("fb_screenshot", None) + st.session_state.fb_paste_key = st.session_state.get("fb_paste_key", 0) + 1 # no st.rerun() — button click already re-renders the dialog # ── Attribution consent ─────────────────────────────────────────────── @@ -217,7 +214,7 @@ def _submit(page, include_diag, submitter, collect_context, collect_logs, def _clear_feedback_state() -> None: for key in [ "fb_step", "fb_type", "fb_title", "fb_desc", "fb_repro", - "fb_diag", "fb_upload", "fb_attr", "fb_screenshot", + "fb_diag", "fb_upload", "fb_attr", "fb_screenshot", "fb_paste_key", ]: st.session_state.pop(key, None)