mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Support submitting feedback
This commit is contained in:
parent
25d17fa0e5
commit
9d010b2884
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
55
supabase/migrations/20240327000000_create_feedback_table.sql
Normal file
55
supabase/migrations/20240327000000_create_feedback_table.sql
Normal 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);
|
||||
56
tests/e2e/feedback.spec.ts
Normal file
56
tests/e2e/feedback.spec.ts
Normal 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();
|
||||
});
|
||||
@ -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')
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user