diff --git a/backend/src/project/project.service.ts b/backend/src/project/project.service.ts index 6cbb1cc9..2299eff5 100644 --- a/backend/src/project/project.service.ts +++ b/backend/src/project/project.service.ts @@ -204,8 +204,36 @@ export class ProjectService { message: input.description, }); + // Create project entity and set properties + const project = new Project(); + project.projectName = projectName; + project.projectPath = ''; + project.userId = userId; + project.isPublic = input.public || false; + project.uniqueProjectId = uuidv4(); + + // Set project packages + try { + project.projectPackages = await this.transformInputToProjectPackages( + input.packages, + ); + } catch (packageError) { + this.logger.error(`Error processing packages: ${packageError.message}`); + // Continue even if packages processing fails + project.projectPackages = []; + } + + // Save project + const savedProject = await this.projectsRepository.save(project); + this.logger.debug(`Project created: ${savedProject.id}`); + // Perform the rest of project creation asynchronously - this.createProjectInBackground(input, projectName, userId, defaultChat); + this.createProjectInBackground( + input, + projectName, + savedProject, + defaultChat, + ); // Return chat immediately so user can start interacting return defaultChat; @@ -226,7 +254,7 @@ export class ProjectService { private async createProjectInBackground( input: CreateProjectInput, projectName: string, - userId: string, + savedProject: Project, chat: Chat, ): Promise { try { @@ -236,31 +264,22 @@ export class ProjectService { projectName, }); const context = new BuilderContext(sequence, sequence.id); - const projectPath = await context.execute(); - // Create project entity and set properties - const project = new Project(); - project.projectName = projectName; - project.projectPath = projectPath; - project.userId = userId; - project.isPublic = input.public || false; - project.uniqueProjectId = uuidv4(); + // Execute the context and update projectPath + const projectPath = await context.execute(); - // Set project packages - try { - project.projectPackages = await this.transformInputToProjectPackages( - input.packages, + if (projectPath) { + savedProject.projectPath = projectPath; + await this.projectsRepository.save(savedProject); // Update the project with path + this.logger.debug( + `Updated project path: ${savedProject.id} -> ${projectPath}`, + ); + } else { + this.logger.error( + `Failed to retrieve project path for: ${savedProject.id}`, ); - } catch (packageError) { - this.logger.error(`Error processing packages: ${packageError.message}`); - // Continue even if packages processing fails - project.projectPackages = []; } - // Save project - const savedProject = await this.projectsRepository.save(project); - this.logger.debug(`Project created: ${savedProject.id}`); - // Bind chat to project const bindSuccess = await this.bindProjectAndChat(savedProject, chat); if (!bindSuccess) { diff --git a/frontend/src/app/(main)/page.tsx b/frontend/src/app/(main)/page.tsx index 60f91e01..a31e4e75 100644 --- a/frontend/src/app/(main)/page.tsx +++ b/frontend/src/app/(main)/page.tsx @@ -7,23 +7,37 @@ import { AuthChoiceModal } from '@/components/auth-choice-modal'; import { useAuthContext } from '@/providers/AuthProvider'; import { ProjectsSection } from '@/components/root/projects-section'; import { PromptForm, PromptFormRef } from '@/components/root/prompt-form'; -import { ProjectContext } from '@/components/chat/code-engine/project-context'; +import { + CreateProjectResult, + ProjectContext, +} from '@/components/chat/code-engine/project-context'; import { SignInModal } from '@/components/sign-in-modal'; import { SignUpModal } from '@/components/sign-up-modal'; import { useRouter } from 'next/navigation'; import { logger } from '../log/logger'; import { AuroraText } from '@/components/magicui/aurora-text'; +import { RateLimitModal } from '@/components/rate-limit-modal'; export default function HomePage() { // States for AuthChoiceModal const [showAuthChoice, setShowAuthChoice] = useState(false); const router = useRouter(); const [showSignIn, setShowSignIn] = useState(false); const [showSignUp, setShowSignUp] = useState(false); + const [showRateLimitModal, setShowRateLimitModal] = useState(false); + const [rateLimiNumber, setRateLimiNumber] = useState( + undefined + ); const promptFormRef = useRef(null); const { isAuthorized } = useAuthContext(); const { createProjectFromPrompt, isLoading } = useContext(ProjectContext); + function isRateLimitResult( + result: CreateProjectResult + ): result is { success: false; rateLimit?: boolean; limitNumber?: number } { + return result.success === false; + } + const handleSubmit = async () => { if (!promptFormRef.current) return; @@ -31,10 +45,16 @@ export default function HomePage() { if (!message.trim()) return; try { - const chatId = await createProjectFromPrompt(message, isPublic, model); - - promptFormRef.current.clearMessage(); - router.push(`/chat?id=${chatId}`); + const result = await createProjectFromPrompt(message, isPublic, model); + + if (result.success && result.chatId) { + promptFormRef.current.clearMessage(); + router.push(`/chat?id=${result.chatId}`); + } else if (isRateLimitResult(result) && result.rateLimit) { + setShowRateLimitModal(true); + setRateLimiNumber(result.limitNumber); + console.log('Rate limit reached ' + result.limitNumber); + } } catch (error) { logger.error('Error creating project:', error); } @@ -178,6 +198,12 @@ export default function HomePage() { setShowSignIn(false)} /> setShowSignUp(false)} /> + + setShowRateLimitModal(false)} + /> ); diff --git a/frontend/src/components/chat/code-engine/project-context.tsx b/frontend/src/components/chat/code-engine/project-context.tsx index f1bcbf2f..05ebc033 100644 --- a/frontend/src/components/chat/code-engine/project-context.tsx +++ b/frontend/src/components/chat/code-engine/project-context.tsx @@ -23,6 +23,10 @@ import { useAuthContext } from '@/providers/AuthProvider'; import { URL_PROTOCOL_PREFIX } from '@/utils/const'; import { logger } from '@/app/log/logger'; +export type CreateProjectResult = + | { success: true; chatId: string } + | { success: false; rateLimit?: boolean; limitNumber?: number }; + export interface ProjectContextType { projects: Project[]; setProjects: React.Dispatch>; @@ -36,7 +40,7 @@ export interface ProjectContextType { prompt: string, isPublic: boolean, model?: string - ) => Promise; + ) => Promise; forkProject: (projectId: string) => Promise; setProjectPublicStatus: ( projectId: string, @@ -104,6 +108,8 @@ export function ProjectProvider({ children }: { children: ReactNode }) { const [projectLoading, setProjectLoading] = useState(true); const [filePath, setFilePath] = useState(null); const [isLoading, setIsLoading] = useState(false); + const rateLimitReachedRef = useRef(false); + const rateLimitValueRef = useRef(undefined); const editorRef = useRef(null); interface ChatProjectCacheEntry { @@ -405,11 +411,24 @@ export function ProjectProvider({ children }: { children: ReactNode }) { // Create project mutation const [createProject] = useMutation(CREATE_PROJECT, { onCompleted: (data) => { + console.log('createProject have yes'); if (!isMounted.current) return; }, onError: (error) => { - if (isMounted.current) { - toast.error(`Failed to create project: ${error.message}`); + const graphqlErrors = error?.graphQLErrors || []; + + const rateLimitError = graphqlErrors.find( + (err) => err.extensions?.code === 'DAILY_LIMIT_EXCEEDED' + ); + + if (rateLimitError) { + rateLimitReachedRef.current = true; + const limitFromBackend = rateLimitError.extensions?.limit; + if (typeof limitFromBackend === 'number') { + rateLimitValueRef.current = limitFromBackend; + } + } else { + logger.error(`GraphQL error: ${error.message}`); } }, }); @@ -686,12 +705,12 @@ export function ProjectProvider({ children }: { children: ReactNode }) { prompt: string, isPublic: boolean, model = 'gpt-4o-mini' - ): Promise => { + ): Promise => { if (!prompt.trim()) { if (isMounted.current) { toast.error('Please enter a project description'); } - return false; + return { success: false }; } try { @@ -699,6 +718,8 @@ export function ProjectProvider({ children }: { children: ReactNode }) { setIsLoading(true); } + rateLimitReachedRef.current = false; + // Default packages based on typical web project needs const defaultPackages = [ { name: 'react', version: '^18.2.0' }, @@ -717,13 +738,31 @@ export function ProjectProvider({ children }: { children: ReactNode }) { }, }); - return result.data.createProject.id; + // Rate limit detected + if (rateLimitReachedRef.current) { + return { + success: false, + rateLimit: true, + limitNumber: rateLimitValueRef.current, + }; + } + + // Check if result and result.data exist before accessing properties + if (result?.data?.createProject?.id) { + return { success: true, chatId: result.data.createProject.id }; + } else { + logger.warn( + 'Project creation response missing expected data structure' + ); + return { success: false }; + } } catch (error) { logger.error('Error creating project:', error); + if (isMounted.current) { toast.error('Failed to create project from prompt'); } - return false; + return { success: false }; } finally { if (isMounted.current) { setIsLoading(false); diff --git a/frontend/src/components/rate-limit-modal.tsx b/frontend/src/components/rate-limit-modal.tsx new file mode 100644 index 00000000..fe868fb4 --- /dev/null +++ b/frontend/src/components/rate-limit-modal.tsx @@ -0,0 +1,58 @@ +// components/rate-limit-modal.tsx +'use client'; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle } from 'lucide-react'; + +interface RateLimitModalProps { + isOpen: boolean; + onClose: () => void; + limit?: number; +} + +export const RateLimitModal = ({ + isOpen, + onClose, + limit = 3, +}: RateLimitModalProps) => { + return ( + !open && onClose()}> + + +
+ + Daily Limit Reached +
+ + You've reached your daily project creation limit. + +
+ +
+

+ Your current plan allows creating up to {limit} projects per day. + This limit resets at midnight UTC. +

+
+

+ Try editing or reusing one of your existing projects, or wait + until tomorrow to create a new one. +

+
+
+ + + + +
+
+ ); +};