Improve save problem

This commit is contained in:
Jason Laster
2025-03-17 19:07:01 -07:00
parent 6d74d355ce
commit 676aa04435
15 changed files with 208 additions and 52 deletions

View File

@@ -47,6 +47,9 @@ jobs:
- name: Run Playwright tests
run: pnpm test:e2e
env:
SUPABASE_TEST_USER_EMAIL: ${{ secrets.SUPABASE_TEST_USER_EMAIL }}
SUPABASE_TEST_USER_PASSWORD: ${{ secrets.SUPABASE_TEST_USER_PASSWORD }}
- name: Upload test results
if: always()
@@ -94,6 +97,8 @@ jobs:
run: pnpm run test:e2e
env:
PLAYWRIGHT_TEST_BASE_URL: ${{ steps.deploy.outputs.WITH_SUPABASE_PREVIEW_URL }}
SUPABASE_TEST_USER_EMAIL: ${{ secrets.SUPABASE_TEST_USER_EMAIL }}
SUPABASE_TEST_USER_PASSWORD: ${{ secrets.SUPABASE_TEST_USER_PASSWORD }}
- name: Upload test results
if: always()

View File

@@ -1,20 +1,19 @@
import ReactModal from 'react-modal';
import { Auth } from './Auth';
import { useStore } from '@nanostores/react';
import { isAuthModalOpenStore, authModalStore } from '~/lib/stores/authModal';
interface AuthModalProps {
isOpen: boolean;
onClose: () => void;
}
export function AuthModal() {
const isOpen = useStore(isAuthModalOpenStore);
export function AuthModal({ isOpen, onClose }: AuthModalProps) {
return (
<ReactModal
isOpen={isOpen}
onRequestClose={onClose}
onRequestClose={authModalStore.close}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 outline-none"
overlayClassName="fixed inset-0 bg-black bg-opacity-50 z-50"
>
<Auth onClose={onClose} />
<Auth onClose={authModalStore.close} />
</ReactModal>
);
}

View File

@@ -14,13 +14,13 @@ export function MessageContents({ message }: MessageContentsProps) {
switch (message.type) {
case 'text':
return (
<div className="overflow-hidden pt-[4px]">
<div data-testid="message-content" className="overflow-hidden pt-[4px]">
<Markdown html>{stripMetadata(message.content)}</Markdown>
</div>
);
case 'image':
return (
<div className="overflow-hidden pt-[4px]">
<div data-testid="message-content" className="overflow-hidden pt-[4px]">
<div className="flex flex-col gap-4">
<img
src={message.dataURL}

View File

@@ -28,6 +28,7 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
return (
<div
data-testid="message"
key={index}
className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),

View File

@@ -20,7 +20,7 @@ export function Header() {
})}
>
<div className="flex flex-1 items-center gap-2 z-logo text-bolt-elements-textPrimary cursor-pointer">
<div className="i-ph:sidebar-simple-duotone text-xl" />
<div data-testid="sidebar-icon" className="i-ph:sidebar-simple-duotone text-xl" />
<a href="/" className="text-2xl font-semibold text-accent flex items-center">
<img src="/logo-styled.svg" alt="logo" className="w-[40px] inline-block rotate-90" />
</a>

View File

@@ -1,10 +1,12 @@
import { toast } from 'react-toastify';
import ReactModal from 'react-modal';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { workbenchStore } from '~/lib/stores/workbench';
import { getProblemsUsername, submitProblem } from '~/lib/replay/Problems';
import { getProblemsUsername, submitProblem, saveProblemsUsername, BoltProblemStatus } from '~/lib/replay/Problems';
import type { BoltProblemInput } from '~/lib/replay/Problems';
import { getRepositoryContents } from '~/lib/replay/Repository';
import { shouldUseSupabase, getCurrentUser, isAuthenticated } from '~/lib/supabase/client';
import { authModalStore } from '~/lib/stores/authModal';
ReactModal.setAppElement('#root');
@@ -15,17 +17,35 @@ export function SaveProblem() {
const [formData, setFormData] = useState({
title: '',
description: '',
name: '',
username: '',
});
const [problemId, setProblemId] = useState<string | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(null);
const handleSaveProblem = () => {
// Check authentication status and get username
useEffect(() => {
async function checkAuthAndUsername() {
if (shouldUseSupabase()) {
const authenticated = await isAuthenticated();
setIsLoggedIn(authenticated);
} else {
setIsLoggedIn(true); // Always considered logged in when not using Supabase
const username = getProblemsUsername();
if (username) {
setFormData((prev) => ({ ...prev, username }));
}
}
}
checkAuthAndUsername();
}, []);
const handleSaveProblem = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setIsModalOpen(true);
setFormData({
title: '',
description: '',
name: '',
});
setProblemId(null);
};
@@ -38,22 +58,22 @@ export function SaveProblem() {
};
const handleSubmitProblem = async () => {
// Add validation here
if (!formData.title) {
toast.error('Please fill in title field');
return;
}
const username = getProblemsUsername();
if (!username) {
toast.error('Please fill in username field');
if (!shouldUseSupabase() && !formData.username) {
toast.error('Please enter a username');
return;
}
toast.info('Submitting problem...');
// Only save username to cookie if not using Supabase
if (!shouldUseSupabase()) {
saveProblemsUsername(formData.username);
}
console.log('SubmitProblem', formData);
toast.info('Submitting problem...');
const repositoryId = workbenchStore.repositoryId.get();
@@ -68,8 +88,10 @@ export function SaveProblem() {
version: 2,
title: formData.title,
description: formData.description,
username,
username: shouldUseSupabase() ? (undefined as any) : formData.username,
user_id: shouldUseSupabase() ? (await getCurrentUser())?.id || '' : undefined,
repositoryContents,
status: BoltProblemStatus.Pending,
};
const problemId = await submitProblem(problem);
@@ -81,20 +103,53 @@ export function SaveProblem() {
return (
<>
<a
href="#"
<button
type="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={handleSaveProblem}
>
Save Problem
</a>
</button>
<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"
shouldCloseOnOverlayClick={true}
shouldCloseOnEsc={true}
style={{
overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 1000,
},
content: {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '20px',
borderRadius: '8px',
maxWidth: '500px',
width: '100%',
},
}}
>
{problemId && (
{shouldUseSupabase() && !isLoggedIn && (
<div className="text-center">
<div className="mb-4">Please log in to save a problem</div>
<button
onClick={() => {
setIsModalOpen(false);
authModalStore.open();
}}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Log In
</button>
</div>
)}
{(!shouldUseSupabase() || isLoggedIn) && problemId && (
<>
<div className="text-center mb-2">Problem Submitted: {problemId}</div>
<div className="text-center">
@@ -109,12 +164,27 @@ export function SaveProblem() {
</div>
</>
)}
{!problemId && (
{(!shouldUseSupabase() || isLoggedIn) && !problemId && (
<>
<div className="text-center">Save prompts as new problems when AI results are unsatisfactory.</div>
<div className="text-center">Problems are publicly visible and are used to improve AI performance.</div>
<div className="text-center">
Save prompts as new problems when AI results are unsatisfactory. Problems are publicly visible and are
used to improve AI performance.
</div>
<div style={{ marginTop: '10px' }}>
<div className="grid grid-cols-[auto_1fr] gap-4 max-w-md mx-auto">
{!shouldUseSupabase() && (
<>
<div className="flex items-center">Username:</div>
<input
type="text"
name="username"
className="bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary rounded px-2 w-full border border-gray-300"
value={formData.username}
onChange={handleInputChange}
/>
</>
)}
<div className="flex items-center">Title:</div>
<input
type="text"

View File

@@ -50,6 +50,7 @@ export interface BoltProblemDescription {
export interface BoltProblem extends BoltProblemDescription {
username?: string;
user_id?: string;
repositoryContents: string;
comments?: BoltProblemComment[];
solution?: BoltProblemSolution;

View File

@@ -0,0 +1,8 @@
import { atom } from 'nanostores';
export const isAuthModalOpenStore = atom(false);
export const authModalStore = {
open: () => isAuthModalOpenStore.set(true),
close: () => isAuthModalOpenStore.set(false),
};

View File

@@ -1,4 +1,5 @@
import { createClient } from '@supabase/supabase-js';
import type { User as SupabaseUser } from '@supabase/supabase-js';
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];
@@ -69,18 +70,30 @@ export function shouldUseSupabase(): boolean {
// URL param takes precedence over environment variable
const shouldUse = useSupabaseFromUrl || useSupabaseFromEnv;
// Log for debugging
if (typeof window !== 'undefined') {
console.log('Supabase usage check:', {
fromUrl: useSupabaseFromUrl,
fromEnv: useSupabaseFromEnv,
enabled: shouldUse,
});
}
return shouldUse;
}
export async function getCurrentUser(): Promise<SupabaseUser | null> {
try {
const {
data: { user },
} = await getSupabase().auth.getUser();
return user;
} catch (error) {
console.error('Error getting current user:', error);
return null;
}
}
/**
* Checks if there is a currently authenticated user.
*/
export async function isAuthenticated(): Promise<boolean> {
const user = await getCurrentUser();
return user !== null;
}
export function getSupabase() {
// Determine execution environment and get appropriate variables
if (typeof window == 'object') {

View File

@@ -127,7 +127,7 @@ export async function supabaseSubmitProblem(problem: BoltProblemInput): Promise<
status: problem.status as BoltProblemStatus,
keywords: problem.keywords || [],
repository_contents: problem.repositoryContents,
user_id: null,
user_id: problem.user_id,
};
const { data, error } = await getSupabase().from('problems').insert(supabaseProblem).select().single();

View File

@@ -12,6 +12,7 @@ import { logStore } from './lib/stores/logs';
import { initializeAuth, userStore, isLoadingStore } from './lib/stores/auth';
import { ToastContainer } from 'react-toastify';
import { Analytics } from '@vercel/analytics/remix';
import { AuthModal } from './components/auth/AuthModal';
import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
import globalStyles from './styles/index.scss?url';
@@ -165,6 +166,7 @@ export default function App() {
<AuthProvider data={data} />
<main className="">{isLoading ? <div></div> : <Outlet />}</main>
<ToastContainer position="bottom-right" theme={theme} />
<AuthModal />
</ClientOnly>
<ScrollRestoration />
<Scripts />

View File

@@ -13,9 +13,10 @@
"test": "vitest --run --exclude tests/e2e",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:supabase": "USE_SUPABASE=true playwright test",
"test:e2e:legacy": "USE_SUPABASE=false playwright test",
"test:e2e:supabase:ui": "USE_SUPABASE=true playwright test --ui",
"test:e2e:legacy:ui": "USE_SUPABASE=false playwright test --ui",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint app",
"lint:fix": "npm run lint -- --fix && prettier app --write",
"start": "remix-serve ./build/server/index.mjs",

View File

@@ -1,4 +1,9 @@
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
if (!process.env.CI) {
dotenv.config({ path: '.env.local' });
}
const port = 5175;
const usePreviewUrl = !!process.env.PLAYWRIGHT_TEST_BASE_URL;

View File

@@ -9,7 +9,7 @@ test('should load the homepage', async ({ page }) => {
await expect(page.locator('header')).toBeVisible();
});
test.skip('Create a project from a preset', async ({ page }) => {
test('Create a project from a preset', async ({ page }) => {
// Using baseURL from config instead of hardcoded URL
await page.goto('/');
await page.getByRole('button', { name: 'Build a todo app in React' }).click();
@@ -18,5 +18,6 @@ test.skip('Create a project from a preset', async ({ page }) => {
.filter({ hasText: /^Build a todo app in React using Tailwind$/ })
.first()
.click();
await page.getByRole('button', { name: 'Code', exact: true }).click();
await expect(page.locator('[data-testid="message"]')).toBeVisible();
});

View File

@@ -1,4 +1,5 @@
import { test, expect } from '@playwright/test';
import { isSupabaseEnabled } from './setup/test-utils';
test('Should be able to load a problem', async ({ page }) => {
await page.goto('/problems');
@@ -7,9 +8,7 @@ test('Should be able to load a problem', async ({ page }) => {
await expect(combobox).toBeVisible({ timeout: 30000 });
await combobox.selectOption('all');
const problem = 'Contact book tiny search icon';
const problemLink = page.getByRole('link', { name: problem }).first();
const problemLink = page.getByRole('link', { name: 'Contact book tiny search icon' }).first();
await expect(problemLink).toBeVisible({ timeout: 30000 });
await problemLink.click();
@@ -19,3 +18,54 @@ test('Should be able to load a problem', async ({ page }) => {
await expect(page.getByText('Import the "problem" folder')).toBeVisible({ timeout: 30000 });
});
test('Should be able to save a problem ', async ({ page }) => {
await page.goto('/problems');
await page.getByRole('link', { name: 'App goes blank getting' }).click();
await page.getByRole('link', { name: 'Load Problem' }).click();
await expect(page.getByText('Import the "problem" folder')).toBeVisible({ timeout: 30000 });
const useSupabase = await isSupabaseEnabled(page);
if (useSupabase) {
await page.locator('[data-testid="sidebar-icon"]').click();
await page.getByRole('button', { name: 'Save Problem' }).click();
await page.getByRole('button', { name: 'Log In' }).click();
await page.getByRole('textbox', { name: 'Email' }).click();
const email = process.env.SUPABASE_TEST_USER_EMAIL || '';
const password = process.env.SUPABASE_TEST_USER_PASSWORD || '';
await page.getByRole('textbox', { name: 'Email' }).fill(email);
await page.getByRole('textbox', { name: 'Email' }).press('Tab');
await page.getByRole('textbox', { name: 'Password' }).fill(password);
await page.getByRole('textbox', { name: 'Password' }).press('Enter');
await expect(page.getByText('Import the "problem" folder')).toBeVisible({ timeout: 30000 });
await page.locator('[data-testid="sidebar-icon"]').click();
await page.getByRole('button', { name: 'Save Problem' }).click();
await page.locator('input[name="title"]').click();
await page.locator('input[name="title"]').fill('[test] playwright');
await page.locator('input[name="description"]').click();
await page.locator('input[name="description"]').fill('...');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('button', { name: 'Close' }).click();
} else {
await page.locator('[data-testid="sidebar-icon"]').click();
await page.getByRole('button', { name: 'Save Problem' }).click();
await page.locator('input[name="title"]').click();
await page.locator('input[name="title"]').fill('[test] playwright');
await page.locator('input[name="description"]').click();
await page.locator('input[name="description"]').fill('...');
await page.locator('input[name="username"]').click();
await page.locator('input[name="username"]').fill('playwright');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('button', { name: 'Close' }).click();
}
});