import { useState, useEffect } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { HotkeyButton } from '@/components/ui/hotkey-button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; import { FolderPlus, FolderOpen, Rocket, ExternalLink, Check, Link, Folder } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { starterTemplates, type StarterTemplate } from '@/lib/templates'; import { getElectronAPI } from '@/lib/electron'; import { cn } from '@/lib/utils'; import { useFileBrowser } from '@/contexts/file-browser-context'; import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config'; const logger = createLogger('NewProjectModal'); interface ValidationErrors { projectName?: boolean; workspaceDir?: boolean; templateSelection?: boolean; customUrl?: boolean; } interface NewProjectModalProps { open: boolean; onOpenChange: (open: boolean) => void; onCreateBlankProject: (projectName: string, parentDir: string) => Promise; onCreateFromTemplate: ( template: StarterTemplate, projectName: string, parentDir: string ) => Promise; onCreateFromCustomUrl: (repoUrl: string, projectName: string, parentDir: string) => Promise; isCreating: boolean; } export function NewProjectModal({ open, onOpenChange, onCreateBlankProject, onCreateFromTemplate, onCreateFromCustomUrl, isCreating, }: NewProjectModalProps) { const [activeTab, setActiveTab] = useState<'blank' | 'template'>('blank'); const [projectName, setProjectName] = useState(''); const [workspaceDir, setWorkspaceDir] = useState(''); const [isLoadingWorkspace, setIsLoadingWorkspace] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState(null); const [useCustomUrl, setUseCustomUrl] = useState(false); const [customUrl, setCustomUrl] = useState(''); const [errors, setErrors] = useState({}); const { openFileBrowser } = useFileBrowser(); // Fetch workspace directory when modal opens useEffect(() => { if (open) { setIsLoadingWorkspace(true); getDefaultWorkspaceDirectory() .then((defaultDir) => { if (defaultDir) { setWorkspaceDir(defaultDir); } }) .catch((error) => { logger.error('Failed to get default workspace directory:', error); }) .finally(() => { setIsLoadingWorkspace(false); }); } }, [open]); // Reset form when modal closes useEffect(() => { if (!open) { setProjectName(''); setSelectedTemplate(null); setUseCustomUrl(false); setCustomUrl(''); setActiveTab('blank'); setErrors({}); } }, [open]); // Clear specific errors when user fixes them useEffect(() => { if (projectName && errors.projectName) { setErrors((prev) => ({ ...prev, projectName: false })); } }, [projectName, errors.projectName]); useEffect(() => { if ((selectedTemplate || (useCustomUrl && customUrl)) && errors.templateSelection) { setErrors((prev) => ({ ...prev, templateSelection: false })); } }, [selectedTemplate, useCustomUrl, customUrl, errors.templateSelection]); useEffect(() => { if (customUrl && errors.customUrl) { setErrors((prev) => ({ ...prev, customUrl: false })); } }, [customUrl, errors.customUrl]); const validateAndCreate = async () => { const newErrors: ValidationErrors = {}; // Check project name if (!projectName.trim()) { newErrors.projectName = true; } // Check workspace dir if (!workspaceDir) { newErrors.workspaceDir = true; } // Check template selection (only for template tab) if (activeTab === 'template') { if (useCustomUrl) { if (!customUrl.trim()) { newErrors.customUrl = true; } } else if (!selectedTemplate) { newErrors.templateSelection = true; } } // If there are errors, show them and don't proceed if (Object.values(newErrors).some(Boolean)) { setErrors(newErrors); return; } // Clear errors and proceed setErrors({}); if (activeTab === 'blank') { await onCreateBlankProject(projectName, workspaceDir); } else if (useCustomUrl && customUrl) { await onCreateFromCustomUrl(customUrl, projectName, workspaceDir); } else if (selectedTemplate) { await onCreateFromTemplate(selectedTemplate, projectName, workspaceDir); } }; const handleOpenRepo = (url: string) => { const api = getElectronAPI(); api.openExternalLink(url); }; const handleSelectTemplate = (template: StarterTemplate) => { setSelectedTemplate(template); setUseCustomUrl(false); setCustomUrl(''); }; const handleToggleCustomUrl = () => { setUseCustomUrl(!useCustomUrl); if (!useCustomUrl) { setSelectedTemplate(null); } }; const handleBrowseDirectory = async () => { const selectedPath = await openFileBrowser({ title: 'Select Base Project Directory', description: 'Choose the parent directory where your project will be created', initialPath: workspaceDir || undefined, }); if (selectedPath) { setWorkspaceDir(selectedPath); // Save to localStorage for next time saveLastProjectDirectory(selectedPath); // Clear any workspace error when a valid directory is selected if (errors.workspaceDir) { setErrors((prev) => ({ ...prev, workspaceDir: false })); } } }; // Use platform-specific path separator const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ? navigator.platform.indexOf('Win') !== -1 ? '\\' : '/' : '/'; const projectPath = workspaceDir && projectName ? `${workspaceDir}${pathSep}${projectName}` : ''; return ( Create New Project Start with a blank project or choose from a starter template. {/* Project Name Input - Always visible at top */}
setProjectName(e.target.value)} className={cn( 'bg-input text-foreground placeholder:text-muted-foreground', errors.projectName ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-border' )} data-testid="project-name-input" autoFocus /> {errors.projectName &&

Project name is required

}
{/* Workspace Directory Display */}
{isLoadingWorkspace ? ( 'Loading workspace...' ) : workspaceDir ? ( <> Will be created at: {projectPath || workspaceDir} ) : null}
setActiveTab(v as 'blank' | 'template')} className="flex-1 flex flex-col overflow-hidden" > Blank Project Starter Kit

Create an empty project with the standard .automaker directory structure. Perfect for starting from scratch or importing an existing codebase.

{/* Error message for template selection */} {errors.templateSelection && (

Please select a template or enter a custom GitHub URL

)} {/* Preset Templates */}
{starterTemplates.map((template) => (
handleSelectTemplate(template)} data-testid={`template-${template.id}`} >

{template.name}

{selectedTemplate?.id === template.id && !useCustomUrl && ( )}

{template.description}

{/* Tech Stack */}
{template.techStack.slice(0, 6).map((tech) => ( {tech} ))} {template.techStack.length > 6 && ( +{template.techStack.length - 6} more )}
{/* Key Features */}
Features: {template.features.slice(0, 3).join(' · ')} {template.features.length > 3 && ` · +${template.features.length - 3} more`}
))} {/* Custom URL Option */}

Custom GitHub URL

{useCustomUrl && }

Clone any public GitHub repository as a starting point.

{useCustomUrl && (
e.stopPropagation()} className="space-y-1"> setCustomUrl(e.target.value)} className={cn( 'bg-input text-foreground placeholder:text-muted-foreground', errors.customUrl ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' : 'border-border' )} data-testid="custom-url-input" /> {errors.customUrl && (

GitHub URL is required

)}
)}
{isCreating ? ( <> {activeTab === 'template' ? 'Cloning...' : 'Creating...'} ) : ( <>Create Project )}
); }