/** * New Project Modal Component * * Multi-step modal for creating new projects: * 1. Enter project name * 2. Select project folder * 3. Choose project template (blank or agentic starter) * 4. Choose spec method (Claude or manual) * 5a. If Claude: Show SpecCreationChat * 5b. If manual: Create project and close */ import { useRef, useState } from 'react' import { createPortal } from 'react-dom' import { Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2, Folder, Zap, FileCode2, AlertCircle, RotateCcw } from 'lucide-react' import { useCreateProject } from '../hooks/useProjects' import { SpecCreationChat } from './SpecCreationChat' import { FolderBrowser } from './FolderBrowser' import { startAgent } from '../lib/api' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Alert, AlertDescription } from '@/components/ui/alert' import { Badge } from '@/components/ui/badge' import { Card, CardContent } from '@/components/ui/card' type InitializerStatus = 'idle' | 'starting' | 'error' type ScaffoldStatus = 'idle' | 'running' | 'success' | 'error' type Step = 'name' | 'folder' | 'template' | 'method' | 'chat' | 'complete' type SpecMethod = 'claude' | 'manual' interface NewProjectModalProps { isOpen: boolean onClose: () => void onProjectCreated: (projectName: string) => void onStepChange?: (step: Step) => void } export function NewProjectModal({ isOpen, onClose, onProjectCreated, onStepChange, }: NewProjectModalProps) { const [step, setStep] = useState('name') const [projectName, setProjectName] = useState('') const [projectPath, setProjectPath] = useState(null) const [_specMethod, setSpecMethod] = useState(null) const [error, setError] = useState(null) const [initializerStatus, setInitializerStatus] = useState('idle') const [initializerError, setInitializerError] = useState(null) const [yoloModeSelected, setYoloModeSelected] = useState(false) const [scaffoldStatus, setScaffoldStatus] = useState('idle') const [scaffoldOutput, setScaffoldOutput] = useState([]) const [scaffoldError, setScaffoldError] = useState(null) const scaffoldLogRef = useRef(null) // Suppress unused variable warning - specMethod may be used in future void _specMethod const createProject = useCreateProject() // Wrapper to notify parent of step changes const changeStep = (newStep: Step) => { setStep(newStep) onStepChange?.(newStep) } if (!isOpen) return null const handleNameSubmit = (e: React.FormEvent) => { e.preventDefault() const trimmed = projectName.trim() if (!trimmed) { setError('Please enter a project name') return } if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) { setError('Project name can only contain letters, numbers, hyphens, and underscores') return } setError(null) changeStep('folder') } const handleFolderSelect = (path: string) => { setProjectPath(path) changeStep('template') } const handleFolderCancel = () => { changeStep('name') } const handleTemplateSelect = async (choice: 'blank' | 'agentic-starter') => { if (choice === 'blank') { changeStep('method') return } if (!projectPath) return setScaffoldStatus('running') setScaffoldOutput([]) setScaffoldError(null) try { const res = await fetch('/api/scaffold/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ template: 'agentic-starter', target_path: projectPath }), }) if (!res.ok || !res.body) { setScaffoldStatus('error') setScaffoldError(`Server error: ${res.status}`) return } const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (!line.startsWith('data: ')) continue try { const event = JSON.parse(line.slice(6)) if (event.type === 'output') { setScaffoldOutput(prev => { const next = [...prev, event.line] return next.length > 100 ? next.slice(-100) : next }) // Auto-scroll setTimeout(() => scaffoldLogRef.current?.scrollTo(0, scaffoldLogRef.current.scrollHeight), 0) } else if (event.type === 'complete') { if (event.success) { setScaffoldStatus('success') setTimeout(() => changeStep('method'), 1200) } else { setScaffoldStatus('error') setScaffoldError(`Scaffold exited with code ${event.exit_code}`) } } else if (event.type === 'error') { setScaffoldStatus('error') setScaffoldError(event.message) } } catch { // skip malformed SSE lines } } } } catch (err) { setScaffoldStatus('error') setScaffoldError(err instanceof Error ? err.message : 'Failed to run scaffold') } } const handleMethodSelect = async (method: SpecMethod) => { setSpecMethod(method) if (!projectPath) { setError('Please select a project folder first') changeStep('folder') return } if (method === 'manual') { // Create project immediately with manual method try { const project = await createProject.mutateAsync({ name: projectName.trim(), path: projectPath, specMethod: 'manual', }) changeStep('complete') setTimeout(() => { onProjectCreated(project.name) handleClose() }, 1500) } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Failed to create project') } } else { // Create project then show chat try { await createProject.mutateAsync({ name: projectName.trim(), path: projectPath, specMethod: 'claude', }) changeStep('chat') } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Failed to create project') } } } const handleSpecComplete = async (_specPath: string, yoloMode: boolean = false) => { // Save yoloMode for retry setYoloModeSelected(yoloMode) // Auto-start the initializer agent setInitializerStatus('starting') try { // Use default concurrency of 3 to match AgentControl.tsx default await startAgent(projectName.trim(), { yoloMode, maxConcurrency: 3, }) // Success - navigate to project changeStep('complete') setTimeout(() => { onProjectCreated(projectName.trim()) handleClose() }, 1500) } catch (err) { setInitializerStatus('error') setInitializerError(err instanceof Error ? err.message : 'Failed to start agent') } } const handleRetryInitializer = () => { setInitializerError(null) setInitializerStatus('idle') handleSpecComplete('', yoloModeSelected) } const handleChatCancel = () => { // Go back to method selection but keep the project changeStep('method') setSpecMethod(null) } const handleExitToProject = () => { // Exit chat and go directly to project - user can start agent manually onProjectCreated(projectName.trim()) handleClose() } const handleClose = () => { changeStep('name') setProjectName('') setProjectPath(null) setSpecMethod(null) setError(null) setInitializerStatus('idle') setInitializerError(null) setYoloModeSelected(false) setScaffoldStatus('idle') setScaffoldOutput([]) setScaffoldError(null) onClose() } const handleBack = () => { if (step === 'method') { changeStep('template') setSpecMethod(null) } else if (step === 'template') { changeStep('folder') setScaffoldStatus('idle') setScaffoldOutput([]) setScaffoldError(null) } else if (step === 'folder') { changeStep('name') setProjectPath(null) } } // Full-screen chat view - use portal to render at body level if (step === 'chat') { return createPortal(
, document.body ) } // Folder step uses larger modal if (step === 'folder') { return ( !open && handleClose()}> {/* Header */}
Select Project Location Select the folder to use for project {projectName}. Create a new folder or choose an existing one.
{/* Folder Browser */}
) } return ( !open && handleClose()}> {step === 'name' && 'Create New Project'} {step === 'template' && 'Choose Project Template'} {step === 'method' && 'Choose Setup Method'} {step === 'complete' && 'Project Created!'} {/* Step 1: Project Name */} {step === 'name' && (
setProjectName(e.target.value)} placeholder="my-awesome-app" pattern="^[a-zA-Z0-9_-]+$" autoFocus />

Use letters, numbers, hyphens, and underscores only.

{error && ( {error} )}
)} {/* Step 2: Project Template */} {step === 'template' && (
{scaffoldStatus === 'idle' && ( <> Start with a blank project or use a pre-configured template.
handleTemplateSelect('blank')} >
Blank Project

Start from scratch. AutoForge will scaffold your app based on the spec you define.

handleTemplateSelect('agentic-starter')} >
Agentic Starter Next.js

Pre-configured Next.js app with BetterAuth, Drizzle ORM, Postgres, and AI capabilities.

)} {scaffoldStatus === 'running' && (
Setting up Agentic Starter...
{scaffoldOutput.map((line, i) => (
{line}
))}
)} {scaffoldStatus === 'success' && (

Template ready!

Proceeding to setup method...

)} {scaffoldStatus === 'error' && (
{scaffoldError || 'An unknown error occurred'} {scaffoldOutput.length > 0 && (
{scaffoldOutput.slice(-10).map((line, i) => (
{line}
))}
)}
)}
)} {/* Step 3: Spec Method */} {step === 'method' && (
How would you like to define your project?
{/* Claude option */} !createProject.isPending && handleMethodSelect('claude')} >
Create with Claude Recommended

Interactive conversation to define features and generate your app specification automatically.

{/* Manual option */} !createProject.isPending && handleMethodSelect('manual')} >
Edit Templates Manually

Edit the template files directly. Best for developers who want full control.

{error && ( {error} )} {createProject.isPending && (
Creating project...
)}
)} {/* Step 3: Complete */} {step === 'complete' && (

{projectName}

Your project has been created successfully!

Redirecting...
)}
) }