diff --git a/frontend-v2/src/lib/components/layout/AppShell.svelte b/frontend-v2/src/lib/components/layout/AppShell.svelte
index 7245ae0..6554d4f 100644
--- a/frontend-v2/src/lib/components/layout/AppShell.svelte
+++ b/frontend-v2/src/lib/components/layout/AppShell.svelte
@@ -56,6 +56,28 @@
let mobileNavOpen = $state(false);
let uploadInput: HTMLInputElement;
+ let feedbackOpen = $state(false);
+ let feedbackText = $state('');
+ let feedbackSending = $state(false);
+ let feedbackSent = $state(false);
+
+ async function sendFeedback() {
+ if (!feedbackText.trim()) return;
+ feedbackSending = true;
+ try {
+ const res = await fetch('/api/feedback', {
+ method: 'POST',
+ credentials: 'include',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ text: feedbackText.trim(), source: 'web' }),
+ });
+ if (res.ok) {
+ feedbackSent = true;
+ setTimeout(() => { feedbackOpen = false; feedbackSent = false; feedbackText = ''; }, 1500);
+ }
+ } catch {}
+ feedbackSending = false;
+ }
let uploadStatus = $state<'' | 'uploading' | 'done'>('');
async function handleUpload(file: File) {
@@ -176,6 +198,10 @@
+
{new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date())}
@@ -246,6 +272,28 @@
+{#if feedbackOpen}
+
+ feedbackOpen = false}>
+
+
e.stopPropagation()}>
+ {#if feedbackSent}
+
+ {:else}
+
+
Bug report, feature request, or anything
+
+
+ {/if}
+
+
+{/if}
+
diff --git a/frontend-v2/src/lib/components/media/DownloadStatusDock.svelte b/frontend-v2/src/lib/components/media/DownloadStatusDock.svelte
index 2bda2ea..b0c807f 100644
--- a/frontend-v2/src/lib/components/media/DownloadStatusDock.svelte
+++ b/frontend-v2/src/lib/components/media/DownloadStatusDock.svelte
@@ -84,12 +84,16 @@
.dock-head,
.dock-card {
- pointer-events: auto;
backdrop-filter: blur(16px);
border: 1px solid rgba(35, 26, 17, 0.1);
box-shadow: 0 24px 40px rgba(35, 24, 15, 0.14);
}
+ .dock-head,
+ .dock-card {
+ pointer-events: none;
+ }
+
.dock-head {
display: flex;
align-items: center;
@@ -209,6 +213,7 @@
}
.dock-action {
+ pointer-events: auto;
justify-self: start;
padding: 8px 12px;
border-radius: 999px;
diff --git a/gateway/feedback.py b/gateway/feedback.py
index d60611f..a1fa1ae 100644
--- a/gateway/feedback.py
+++ b/gateway/feedback.py
@@ -98,10 +98,39 @@ def handle_feedback(handler, body, user):
)
resp = urllib.request.urlopen(req, timeout=10)
result = json.loads(resp.read())
+ issue_number = result.get("number")
+
+ # Upload image attachment if provided
+ image_b64 = data.get("image")
+ if image_b64 and issue_number:
+ import base64
+ try:
+ img_bytes = base64.b64decode(image_b64)
+ boundary = "----FeedbackBoundary123"
+ body_parts = []
+ body_parts.append(f"--{boundary}\r\n".encode())
+ body_parts.append(b'Content-Disposition: form-data; name="attachment"; filename="screenshot.png"\r\n')
+ body_parts.append(b"Content-Type: image/png\r\n\r\n")
+ body_parts.append(img_bytes)
+ body_parts.append(f"\r\n--{boundary}--\r\n".encode())
+ multipart_body = b"".join(body_parts)
+
+ img_req = urllib.request.Request(
+ f"{GITEA_URL}/api/v1/repos/{REPO}/issues/{issue_number}/assets",
+ data=multipart_body,
+ headers={
+ "Authorization": f"token {GITEA_TOKEN}",
+ "Content-Type": f"multipart/form-data; boundary={boundary}",
+ },
+ method="POST",
+ )
+ urllib.request.urlopen(img_req, timeout=15)
+ except Exception as img_err:
+ pass # Image upload failed but issue was created
handler._send_json({
"ok": True,
- "issue_number": result.get("number"),
+ "issue_number": issue_number,
"url": result.get("html_url"),
})
except Exception as e:
diff --git a/ios/Platform/Platform/Features/Feedback/FeedbackView.swift b/ios/Platform/Platform/Features/Feedback/FeedbackView.swift
index 39057cc..091fd69 100644
--- a/ios/Platform/Platform/Features/Feedback/FeedbackView.swift
+++ b/ios/Platform/Platform/Features/Feedback/FeedbackView.swift
@@ -1,4 +1,5 @@
import SwiftUI
+import PhotosUI
struct FeedbackButton: View {
@State private var showSheet = false
@@ -15,7 +16,7 @@ struct FeedbackButton: View {
}
.sheet(isPresented: $showSheet) {
FeedbackSheet()
- .presentationDetents([.medium])
+ .presentationDetents([.large, .medium])
}
}
}
@@ -26,6 +27,9 @@ struct FeedbackSheet: View {
@State private var isSending = false
@State private var sent = false
@State private var error: String?
+ @State private var selectedPhoto: PhotosPickerItem?
+ @State private var photoData: Data?
+ @State private var photoPreview: UIImage?
var body: some View {
VStack(spacing: 16) {
@@ -64,6 +68,43 @@ struct FeedbackSheet: View {
.stroke(Color.textTertiary.opacity(0.2), lineWidth: 1)
)
+ // Photo attachment
+ HStack {
+ PhotosPicker(selection: $selectedPhoto, matching: .screenshots) {
+ HStack(spacing: 6) {
+ Image(systemName: "camera.fill")
+ .font(.caption)
+ Text(photoPreview != nil ? "Change photo" : "Add screenshot")
+ .font(.caption.weight(.medium))
+ }
+ .foregroundStyle(Color.accentWarm)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ .background(Color.accentWarm.opacity(0.1))
+ .clipShape(Capsule())
+ }
+
+ if let preview = photoPreview {
+ Image(uiImage: preview)
+ .resizable()
+ .scaledToFill()
+ .frame(width: 40, height: 40)
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+
+ Button {
+ photoData = nil
+ photoPreview = nil
+ selectedPhoto = nil
+ } label: {
+ Image(systemName: "xmark.circle.fill")
+ .font(.caption)
+ .foregroundStyle(Color.textTertiary)
+ }
+ }
+
+ Spacer()
+ }
+
if let error {
Text(error)
.font(.caption)
@@ -90,6 +131,15 @@ struct FeedbackSheet: View {
}
.padding()
.background(Color.surfaceCard)
+ .onChange(of: selectedPhoto) {
+ Task {
+ if let item = selectedPhoto,
+ let data = try? await item.loadTransferable(type: Data.self) {
+ photoData = data
+ photoPreview = UIImage(data: data)
+ }
+ }
+ }
}
private func sendFeedback() {
@@ -98,7 +148,10 @@ struct FeedbackSheet: View {
Task {
do {
- let body: [String: String] = ["text": text.trimmingCharacters(in: .whitespaces), "source": "ios"]
+ var body: [String: Any] = ["text": text.trimmingCharacters(in: .whitespaces), "source": "ios"]
+ if let imgData = photoData {
+ body["image"] = imgData.base64EncodedString()
+ }
let jsonData = try JSONSerialization.data(withJSONObject: body)
let data = try await APIClient.shared.rawPost(path: "/api/feedback", data: jsonData)