feat: instant feedback button — creates Gitea issues with auto-labels
iOS: - Subtle floating feedback button (bottom-left, speech bubble icon) - Quick sheet: type → send → auto-creates Gitea issue - Shows checkmark on success, auto-dismisses - No friction — tap, type, done Gateway: - POST /api/feedback endpoint - Auto-labels: bug/feature/enhancement + ios/web + fitness/brain/reader/podcasts - Keyword detection for label assignment - Creates issue via Gitea API with user name and source Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
gateway/feedback.py
Normal file
108
gateway/feedback.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Platform Gateway — Feedback handler.
|
||||
Creates Gitea issues from user feedback with auto-labeling.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
GITEA_URL = "http://localhost:3300"
|
||||
GITEA_TOKEN = "7abae0245e49e130e1b6752b7fa381a57607d8c7"
|
||||
REPO = "yusiboyz/platform"
|
||||
|
||||
# Label IDs from Gitea
|
||||
LABELS = {
|
||||
"bug": 11,
|
||||
"feature": 12,
|
||||
"ios": 13,
|
||||
"web": 14,
|
||||
"fitness": 15,
|
||||
"brain": 16,
|
||||
"reader": 17,
|
||||
"podcasts": 18,
|
||||
"enhancement": 19,
|
||||
}
|
||||
|
||||
|
||||
def auto_label(text: str, source: str = "ios") -> list[int]:
|
||||
"""Detect labels from feedback text."""
|
||||
text_lower = text.lower()
|
||||
label_ids = []
|
||||
|
||||
# Source platform
|
||||
if source == "ios":
|
||||
label_ids.append(LABELS["ios"])
|
||||
elif source == "web":
|
||||
label_ids.append(LABELS["web"])
|
||||
|
||||
# Bug or feature
|
||||
bug_words = ["bug", "broken", "crash", "error", "wrong", "fix", "issue", "doesn't work", "not working", "can't"]
|
||||
feature_words = ["want", "add", "wish", "would be nice", "can we", "can you", "feature", "idea", "suggest"]
|
||||
|
||||
if any(w in text_lower for w in bug_words):
|
||||
label_ids.append(LABELS["bug"])
|
||||
elif any(w in text_lower for w in feature_words):
|
||||
label_ids.append(LABELS["feature"])
|
||||
else:
|
||||
label_ids.append(LABELS["enhancement"])
|
||||
|
||||
# App detection
|
||||
if any(w in text_lower for w in ["fitness", "calorie", "food", "meal", "macro", "protein"]):
|
||||
label_ids.append(LABELS["fitness"])
|
||||
elif any(w in text_lower for w in ["brain", "note", "save", "bookmark"]):
|
||||
label_ids.append(LABELS["brain"])
|
||||
elif any(w in text_lower for w in ["reader", "rss", "feed", "article"]):
|
||||
label_ids.append(LABELS["reader"])
|
||||
elif any(w in text_lower for w in ["podcast", "episode", "listen", "player"]):
|
||||
label_ids.append(LABELS["podcasts"])
|
||||
|
||||
return label_ids
|
||||
|
||||
|
||||
def handle_feedback(handler, body, user):
|
||||
"""Create a Gitea issue from user feedback."""
|
||||
try:
|
||||
data = json.loads(body)
|
||||
except Exception:
|
||||
handler._send_json({"error": "Invalid JSON"}, 400)
|
||||
return
|
||||
|
||||
text = (data.get("text") or "").strip()
|
||||
source = data.get("source", "ios")
|
||||
|
||||
if not text:
|
||||
handler._send_json({"error": "Feedback text is required"}, 400)
|
||||
return
|
||||
|
||||
user_name = user.get("display_name") or user.get("username", "Unknown")
|
||||
label_ids = auto_label(text, source)
|
||||
|
||||
# Create Gitea issue
|
||||
issue_body = {
|
||||
"title": text[:80] + ("..." if len(text) > 80 else ""),
|
||||
"body": f"**From:** {user_name} ({source})\n\n{text}",
|
||||
"labels": label_ids,
|
||||
}
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{GITEA_URL}/api/v1/repos/{REPO}/issues",
|
||||
data=json.dumps(issue_body).encode(),
|
||||
headers={
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
result = json.loads(resp.read())
|
||||
|
||||
handler._send_json({
|
||||
"ok": True,
|
||||
"issue_number": result.get("number"),
|
||||
"url": result.get("html_url"),
|
||||
})
|
||||
except Exception as e:
|
||||
handler._send_json({"error": f"Failed to create issue: {e}"}, 500)
|
||||
Reference in New Issue
Block a user