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)