mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Improve save problem
This commit is contained in:
5
.github/workflows/playwright.yaml
vendored
5
.github/workflows/playwright.yaml
vendored
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface BoltProblemDescription {
|
||||
|
||||
export interface BoltProblem extends BoltProblemDescription {
|
||||
username?: string;
|
||||
user_id?: string;
|
||||
repositoryContents: string;
|
||||
comments?: BoltProblemComment[];
|
||||
solution?: BoltProblemSolution;
|
||||
|
||||
8
app/lib/stores/authModal.ts
Normal file
8
app/lib/stores/authModal.ts
Normal 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),
|
||||
};
|
||||
@@ -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') {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user