Support submitting feedback

This commit is contained in:
Jason Laster 2025-03-11 14:30:00 -04:00
parent 25d17fa0e5
commit 9d010b2884
11 changed files with 323 additions and 115 deletions

View File

@ -3,6 +3,7 @@ import ReactModal from 'react-modal';
import { useState } from 'react';
import { submitFeedback } from '~/lib/replay/Problems';
import { getLastProjectContents, getLastChatMessages } from '~/components/chat/Chat.client';
import { shouldUseSupabase } from '~/lib/supabase/client';
ReactModal.setAppElement('#root');
@ -11,7 +12,7 @@ ReactModal.setAppElement('#root');
export function Feedback() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState({
feedback: '',
description: '',
email: '',
share: false,
});
@ -20,7 +21,7 @@ export function Feedback() {
const handleOpenModal = () => {
setIsModalOpen(true);
setFormData({
feedback: '',
description: '',
email: '',
share: false,
});
@ -28,127 +29,165 @@ export function Feedback() {
};
const handleSubmitFeedback = async () => {
// Add validation here
if (!formData.feedback) {
toast.error('Please fill in feedback field');
if (!formData.description) {
toast.error('Please fill in the feedback field');
return;
}
if (!formData.email) {
toast.error('Please fill in email field');
if (!shouldUseSupabase() && !formData.email) {
toast.error('Please fill in the email field');
return;
}
toast.info('Submitting feedback...');
console.log('SubmitFeedback', formData);
const feedbackData: any = {
feedback: formData.feedback,
email: formData.email,
share: formData.share,
};
const feedbackData: any = shouldUseSupabase()
? {
description: formData.description,
share: formData.share,
source: 'feedback_modal',
}
: {
feedback: formData.description,
email: formData.email,
share: formData.share,
};
if (feedbackData.share) {
// Note: We don't just use the workbench store here because wrangler generates a strange error.
feedbackData.repositoryContents = getLastProjectContents();
feedbackData.chatMessages = getLastChatMessages();
}
const success = await submitFeedback(feedbackData);
try {
const success = await submitFeedback(feedbackData);
if (success) {
setSubmitted(true);
if (success) {
setSubmitted(true);
toast.success('Feedback submitted successfully!');
} else {
toast.error('Failed to submit feedback');
}
} catch (error) {
console.error('Error submitting feedback:', error);
toast.error('An error occurred while submitting feedback');
}
};
return (
<>
<a
href="#"
<button
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
onClick={handleOpenModal}
onClick={() => {
handleOpenModal();
}}
>
Feedback
</a>
<ReactModal
isOpen={isModalOpen}
onRequestClose={() => setIsModalOpen(false)}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 max-w-2xl w-full z-50"
overlayClassName="fixed inset-0 bg-black bg-opacity-50 z-40"
>
{submitted && (
<>
<div className="text-center mb-2">Feedback Submitted</div>
<div className="text-center">
<div className="flex justify-center gap-2 mt-4">
<button
onClick={() => setIsModalOpen(false)}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Close
</button>
</div>
</div>
</>
)}
{!submitted && (
<>
<div className="text-center">Let us know how Nut is doing.</div>
<div className="flex items-center">Feedback:</div>
<textarea
name="feedback"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.feedback}
onChange={(e) =>
setFormData((prev) => ({
...prev,
feedback: e.target.value,
}))
}
/>
<div className="flex items-center">Email:</div>
<input
type="text"
name="email"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.email}
onChange={(e) =>
setFormData((prev) => ({
...prev,
email: e.target.value,
}))
}
/>
<div className="flex items-center gap-2">
<span>Share project with the Nut team:</span>
<input
type="checkbox"
name="share"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded border border-gray-300"
checked={formData.share}
onChange={(e) =>
setFormData((prev) => ({
...prev,
share: e.target.checked,
}))
}
/>
</div>
<div className="flex justify-center gap-2 mt-4">
<button
onClick={handleSubmitFeedback}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Submit
</button>
<button onClick={() => setIsModalOpen(false)} className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">
Cancel
</button>
</div>
</>
)}
</ReactModal>
</button>
{isModalOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full z-50">
{submitted ? (
<>
<div className="text-center mb-2">Feedback Submitted</div>
<div className="text-center">
<p className="text-gray-600 mb-4">Thank you for your feedback! We appreciate your input.</p>
<div className="flex justify-center gap-2 mt-4">
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400"
>
Close
</button>
</div>
</div>
</>
) : (
<>
<h2 className="text-xl font-semibold text-center mb-4">Share Your Feedback</h2>
<div className="text-center mb-4">
Let us know how Nut is doing or report any issues you've encountered.
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Your Feedback:</label>
<textarea
name="description"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 w-full border border-gray-300 min-h-[120px]"
value={formData.description}
placeholder="Tell us what you think or describe any issues..."
onChange={(e) => {
setFormData((prev) => ({
...prev,
description: e.target.value,
}));
}}
/>
</div>
{!shouldUseSupabase() && (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Your Email:</label>
<input
type="email"
name="email"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 py-2 w-full border border-gray-300"
value={formData.email}
placeholder="Enter your email address"
onChange={(e) => {
setFormData((prev) => ({
...prev,
email: e.target.value,
}));
}}
/>
</div>
)}
<div className="flex items-center gap-2 mb-6">
<input
type="checkbox"
id="share-project"
name="share"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded border border-gray-300"
checked={formData.share}
onChange={(e) => {
setFormData((prev) => ({
...prev,
share: e.target.checked,
}));
}}
/>
<label htmlFor="share-project" className="text-sm text-gray-700">
Share project with the Nut team (helps us diagnose issues)
</label>
</div>
<div className="flex justify-center gap-2 mt-4">
<button
onClick={handleSubmitFeedback}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Submit Feedback
</button>
<button
onClick={() => {
setIsModalOpen(false);
}}
className="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400 transition-colors"
>
Cancel
</button>
</div>
</>
)}
</div>
</div>
)}
</>
);
}

View File

@ -261,7 +261,7 @@ export async function extractFileArtifactsFromRepositoryContents(repositoryConte
return fileArtifacts;
}
export async function submitFeedback(feedback: any) {
export async function submitFeedback(feedback: any): Promise<boolean> {
if (shouldUseSupabase()) {
return supabaseSubmitFeedback(feedback);
}

View File

@ -32,6 +32,19 @@ export interface Database {
Insert: Omit<Database['public']['Tables']['problem_comments']['Row'], 'id' | 'created_at'>;
Update: Partial<Database['public']['Tables']['problem_comments']['Insert']>;
};
feedback: {
Row: {
id: string;
created_at: string;
updated_at: string;
user_id: string | null;
description: string;
status: 'pending' | 'reviewed' | 'resolved';
metadata: Json;
};
Insert: Omit<Database['public']['Tables']['feedback']['Row'], 'id' | 'created_at' | 'updated_at'>;
Update: Partial<Database['public']['Tables']['feedback']['Insert']>;
};
};
};
}

View File

@ -228,18 +228,29 @@ export async function supabaseUpdateProblem(problemId: string, problem: BoltProb
}
export async function supabaseSubmitFeedback(feedback: any) {
try {
/*
* Store feedback in supabase if needed
* For now, just return true
*/
console.log('Feedback submitted:', feedback);
const supabase = getSupabase();
return true;
} catch (error) {
console.error('Error submitting feedback', error);
// Get the current user ID if available
const {
data: { user },
} = await supabase.auth.getUser();
const userId = user?.id || null;
// Insert feedback into the feedback table
const { data, error } = await supabase.from('feedback').insert({
user_id: userId,
description: feedback.description || feedback.text || JSON.stringify(feedback),
metadata: feedback,
});
if (error) {
console.error('Error submitting feedback to Supabase:', error);
toast.error('Failed to submit feedback');
return false;
}
console.log('Feedback submitted successfully:', data);
return true;
}

View File

@ -14,6 +14,9 @@
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:supabase": "playwright test --project=chromium-supabase-true",
"test:e2e:no-supabase": "playwright test --project=chromium-supabase-false",
"test:e2e:all": "playwright test --project=chromium-supabase-true --project=chromium-supabase-false",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint app",
"lint:fix": "npm run lint -- --fix && prettier app --write",
"start:windows": "wrangler pages dev ./build/client",

View File

@ -8,7 +8,7 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
baseURL: 'http://localhost:5174',
trace: 'on-first-retry',
},
projects: [
@ -19,7 +19,7 @@ export default defineConfig({
],
webServer: {
command: 'pnpm run dev',
port: 5173,
port: 5174,
reuseExistingServer: !process.env.CI,
},
});
});

View File

@ -0,0 +1,55 @@
-- Create feedback table
CREATE TABLE IF NOT EXISTS public.feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
user_id UUID REFERENCES auth.users(id),
description TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'resolved')),
metadata JSONB DEFAULT '{}'
);
-- Create updated_at trigger for feedback table
CREATE TRIGGER update_feedback_updated_at
BEFORE UPDATE ON public.feedback
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Enable Row Level Security
ALTER TABLE public.feedback ENABLE ROW LEVEL SECURITY;
-- Create policies for feedback table
-- Allow public read access to feedback for admins
CREATE POLICY "Allow admin read access to all feedback"
ON public.feedback FOR SELECT
TO public
USING (
auth.uid() IN (SELECT id FROM public.profiles WHERE is_admin = true)
);
-- Allow users to read their own feedback
CREATE POLICY "Allow users to read their own feedback"
ON public.feedback FOR SELECT
TO public
USING (auth.uid() = user_id);
-- Allow anyone to create feedback (including anonymous users)
CREATE POLICY "Allow anyone to create feedback"
ON public.feedback FOR INSERT
TO public
WITH CHECK (true);
-- Allow admins to update any feedback
CREATE POLICY "Allow admins to update any feedback"
ON public.feedback FOR UPDATE
TO public
USING (
auth.uid() IN (SELECT id FROM public.profiles WHERE is_admin = true)
);
-- Allow users to update their own feedback
CREATE POLICY "Allow users to update their own feedback"
ON public.feedback FOR UPDATE
TO public
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

View File

@ -0,0 +1,56 @@
import { test, expect } from '@playwright/test';
import { isSupabaseEnabled } from './setup/test-utils';
test.beforeEach(async () => {
// Log Supabase status at the start of each test
const useSupabase = isSupabaseEnabled();
console.log(`Test running with USE_SUPABASE=${useSupabase}`);
});
test('should submit feedback', async ({ page }) => {
// Navigate to the homepage
// The URL will automatically use the baseURL from the config
await page.goto('/');
// Get Supabase status from environment variable
const useSupabase = isSupabaseEnabled();
// Click on the Feedback button
await page.getByRole('button', { name: 'Feedback' }).click();
// Verify the feedback modal is open
await expect(page.getByText('Share Your Feedback')).toBeVisible();
// Prepare feedback message
const feedbackMessage = useSupabase
? '[test] This is a test feedback message with Supabase'
: 'This is a test feedback message';
await page.locator('textarea[name="description"]').fill(feedbackMessage);
// If email field is required (when not using Supabase), fill it
const emailField = page.locator('input[type="email"][name="email"]');
if (await emailField.isVisible()) {
// We expect email field to be visible when NOT using Supabase
expect(useSupabase).toBe(false);
await emailField.fill('test@example.com');
} else {
// We expect email field to NOT be visible when using Supabase
expect(useSupabase).toBe(true);
}
// Check the share project checkbox if Supabase is enabled
if (useSupabase) {
await page.locator('input[type="checkbox"][name="share"]').check();
}
// Submit the feedback
await page.getByRole('button', { name: 'Submit Feedback' }).click();
// Wait for the success message in the modal
await expect(page.locator('div.text-center.mb-2').filter({ hasText: 'Feedback Submitted' })).toBeVisible({
timeout: 10000,
});
await expect(page.getByText('Thank you for your feedback!')).toBeVisible();
});

View File

@ -1,6 +1,14 @@
import { test, expect } from '@playwright/test';
import { isSupabaseEnabled } from './setup/test-utils';
test.beforeEach(async () => {
// Log Supabase status at the start of each test
const useSupabase = isSupabaseEnabled();
console.log(`Test running with USE_SUPABASE=${useSupabase}`);
});
test('should load the homepage', async ({ page }) => {
// Using baseURL from config
await page.goto('/');
const title = await page.title();
@ -9,7 +17,8 @@ test('should load the homepage', async ({ page }) => {
});
test('Create a project from a preset', async ({ page }) => {
await page.goto('http://localhost:5173/');
// Using baseURL from config instead of hardcoded URL
await page.goto('/');
await page.getByRole('button', { name: 'Build a todo app in React' }).click();
await page
.locator('div')

View File

@ -1,8 +1,23 @@
import { test, expect } from '@playwright/test';
import { isSupabaseEnabled } from './setup/test-utils';
const problemName = {
false: 'Contact book tiny search icon',
true: 'sdfsdf',
};
test.beforeEach(async () => {
// Log Supabase status at the start of each test
const useSupabase = isSupabaseEnabled();
console.log(`Test running with USE_SUPABASE=${useSupabase}`);
});
test('Should be able to load a problem', async ({ page }) => {
await page.goto('/problems');
await page.getByRole('link', { name: 'Contact book tiny search icon' }).click();
await page.getByRole('combobox').selectOption('all');
const problem = problemName[isSupabaseEnabled()];
await page.getByRole('link', { name: problem }).first().click();
await page.getByRole('link', { name: 'Load Problem' }).click();
await expect(page.getByText('Import the "problem" folder')).toBeVisible();
});

View File

@ -1,6 +1,13 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
/**
* Checks if Supabase is enabled based on the environment variable
*/
export function isSupabaseEnabled(): boolean {
return process.env.USE_SUPABASE === 'true';
}
/**
* Waits for the page to be fully loaded
*/