-
-
+ {error &&
{error}
}
+
+
+ {/* Subtle attach button */}
+
+
{
+ if (e.target.files) addFiles(e.target.files)
+ e.target.value = ''
+ }}
+ />
+
+
+
+
+
)}
diff --git a/apps/editor/features/community/lib/feedback/actions.ts b/apps/editor/features/community/lib/feedback/actions.ts
index b297e390..8dabe9f4 100644
--- a/apps/editor/features/community/lib/feedback/actions.ts
+++ b/apps/editor/features/community/lib/feedback/actions.ts
@@ -4,12 +4,68 @@ import { createId } from '@pascal-app/db'
import { createServerSupabaseClient } from '../database/server'
import { getSession } from '../auth/server'
-export async function submitFeedback(
- message: string,
-): Promise<{ success: true } | { success: false; error: string }> {
+const MAX_IMAGES = 5
+
+/**
+ * Create signed upload URLs so the client can upload images directly to
+ * Supabase Storage — bypasses Vercel's 4.5 MB serverless body-size limit.
+ */
+export async function createImageUploadUrls(
+ files: { name: string; type: string }[],
+): Promise<
+ | { success: true; uploads: { path: string; signedUrl: string }[] }
+ | { success: false; error: string }
+> {
try {
- const trimmed = message.trim()
- if (!trimmed) return { success: false, error: 'Message cannot be empty' }
+ if (files.length > MAX_IMAGES) {
+ return { success: false, error: `Maximum ${MAX_IMAGES} images allowed` }
+ }
+
+ const supabase = await createServerSupabaseClient()
+ const uploads: { path: string; signedUrl: string }[] = []
+
+ for (const file of files) {
+ if (!file.type.startsWith('image/')) continue
+
+ const ext = file.name.split('.').pop() || 'jpg'
+ const path = `${createId('img')}.${ext}`
+
+ const { data, error } = await (
+ supabase as ReturnType
+ ).storage
+ .from('feedback-images')
+ .createSignedUploadUrl(path)
+
+ if (error || !data) {
+ console.error(`Failed to create signed URL for ${file.name}:`, error)
+ continue
+ }
+
+ uploads.push({ path, signedUrl: data.signedUrl })
+ }
+
+ return { success: true, uploads }
+ } catch (err) {
+ return {
+ success: false,
+ error: err instanceof Error ? err.message : 'Failed to create upload URLs',
+ }
+ }
+}
+
+/**
+ * Submit feedback with pre-uploaded image paths.
+ * Images are already in Supabase Storage — this just records the metadata.
+ */
+export async function submitFeedback(data: {
+ message: string
+ projectId?: string | null
+ sceneGraph?: unknown
+ imagePaths?: string[]
+}): Promise<{ success: true } | { success: false; error: string }> {
+ try {
+ const { message, projectId, sceneGraph, imagePaths } = data
+ if (!message?.trim()) return { success: false, error: 'Message cannot be empty' }
const session = await getSession()
const supabase = await createServerSupabaseClient()
@@ -18,7 +74,10 @@ export async function submitFeedback(
const { error } = await (supabase as any).from('feedback').insert({
id: createId('feedback'),
user_id: session?.user?.id ?? null,
- message: trimmed,
+ project_id: projectId ?? null,
+ message: message.trim(),
+ images: imagePaths && imagePaths.length > 0 ? imagePaths : null,
+ scene_graph: sceneGraph ?? null,
})
if (error) return { success: false, error: error.message }
diff --git a/packages/db/src/schema/feedback/feedback.ts b/packages/db/src/schema/feedback/feedback.ts
index f49931e0..5dea8607 100644
--- a/packages/db/src/schema/feedback/feedback.ts
+++ b/packages/db/src/schema/feedback/feedback.ts
@@ -4,8 +4,11 @@ import { id, createdAt } from '../../helpers'
export const feedback = pgTable('feedback', (t) => ({
id: id('feedback'),
- userId: t.text('user_id'), // nullable — stores Better Auth user ID or null for anonymous
+ userId: t.text('user_id'),
+ projectId: t.text('project_id'),
message: t.text('message').notNull(),
+ images: t.jsonb('images').$type(),
+ sceneGraph: t.jsonb('scene_graph'),
createdAt,
})).enableRLS()
diff --git a/supabase/migrations/20260223020000_feedback_images_and_context.sql b/supabase/migrations/20260223020000_feedback_images_and_context.sql
new file mode 100644
index 00000000..84422634
--- /dev/null
+++ b/supabase/migrations/20260223020000_feedback_images_and_context.sql
@@ -0,0 +1,20 @@
+-- Add image upload, project context, and scene graph to feedback
+ALTER TABLE feedback
+ ADD COLUMN IF NOT EXISTS project_id text,
+ ADD COLUMN IF NOT EXISTS images jsonb,
+ ADD COLUMN IF NOT EXISTS scene_graph jsonb;
+
+-- Create feedback-images storage bucket (public read, service-role write)
+INSERT INTO storage.buckets (id, name, public)
+VALUES ('feedback-images', 'feedback-images', true)
+ON CONFLICT (id) DO NOTHING;
+
+-- Allow public read on feedback-images bucket
+CREATE POLICY "Public read feedback images"
+ ON storage.objects FOR SELECT
+ USING (bucket_id = 'feedback-images');
+
+-- Allow service role (and authenticated users) to upload
+CREATE POLICY "Service role upload feedback images"
+ ON storage.objects FOR INSERT
+ WITH CHECK (bucket_id = 'feedback-images');