import { useState, useCallback, useEffect } from 'react'; import { Plus, Bug, FolderOpen, BookOpen } from 'lucide-react'; import { useNavigate, useLocation } from '@tanstack/react-router'; import { cn } from '@/lib/utils'; import { useAppStore, type ThemeMode } from '@/store/app-store'; import { useOSDetection } from '@/hooks/use-os-detection'; import { ProjectSwitcherItem } from './components/project-switcher-item'; import { ProjectContextMenu } from './components/project-context-menu'; import { EditProjectDialog } from './components/edit-project-dialog'; import { NotificationBell } from './components/notification-bell'; import { NewProjectModal } from '@/components/dialogs/new-project-modal'; import { OnboardingDialog } from '@/components/layout/sidebar/dialogs'; import { useProjectCreation, useProjectTheme } from '@/components/layout/sidebar/hooks'; import { SIDEBAR_FEATURE_FLAGS } from '@/components/layout/sidebar/constants'; import type { Project } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron'; import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init'; import { toast } from 'sonner'; import { CreateSpecDialog } from '@/components/views/spec-view/dialogs'; import type { FeatureCount } from '@/components/views/spec-view/types'; function getOSAbbreviation(os: string): string { switch (os) { case 'mac': return 'M'; case 'windows': return 'W'; case 'linux': return 'L'; default: return '?'; } } export function ProjectSwitcher() { const navigate = useNavigate(); const location = useLocation(); const { hideWiki } = SIDEBAR_FEATURE_FLAGS; const isWikiActive = location.pathname === '/wiki'; const { projects, currentProject, setCurrentProject, trashedProjects, upsertAndSetCurrentProject, specCreatingForProject, setSpecCreatingForProject, } = useAppStore(); const [contextMenuProject, setContextMenuProject] = useState(null); const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>( null ); const [editDialogProject, setEditDialogProject] = useState(null); // Setup dialog state for opening existing projects const [showSetupDialog, setShowSetupDialog] = useState(false); const [setupProjectPath, setSetupProjectPath] = useState(null); const [projectOverview, setProjectOverview] = useState(''); const [generateFeatures, setGenerateFeatures] = useState(true); const [analyzeProject, setAnalyzeProject] = useState(true); const [featureCount, setFeatureCount] = useState(50); // Derive isCreatingSpec from store state const isCreatingSpec = specCreatingForProject !== null; // Version info const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0'; const { os } = useOSDetection(); const appMode = import.meta.env.VITE_APP_MODE || '?'; const versionSuffix = `${getOSAbbreviation(os)}${appMode}`; // Get global theme for project creation const { globalTheme } = useProjectTheme(); // Project creation state and handlers const { showNewProjectModal, setShowNewProjectModal, isCreatingProject, showOnboardingDialog, setShowOnboardingDialog, newProjectName, handleCreateBlankProject, handleCreateFromTemplate, handleCreateFromCustomUrl, } = useProjectCreation({ trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject, }); const handleContextMenu = (project: Project, event: React.MouseEvent) => { event.preventDefault(); setContextMenuProject(project); setContextMenuPosition({ x: event.clientX, y: event.clientY }); }; const handleCloseContextMenu = () => { setContextMenuProject(null); setContextMenuPosition(null); }; const handleEditProject = (project: Project) => { setEditDialogProject(project); handleCloseContextMenu(); }; const handleProjectClick = useCallback( (project: Project) => { setCurrentProject(project); // Navigate to board view when switching projects navigate({ to: '/board' }); }, [setCurrentProject, navigate] ); const handleNewProject = () => { // Open the new project modal setShowNewProjectModal(true); }; const handleOnboardingSkip = () => { setShowOnboardingDialog(false); navigate({ to: '/board' }); }; const handleBugReportClick = useCallback(() => { const api = getElectronAPI(); api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues'); }, []); const handleWikiClick = useCallback(() => { navigate({ to: '/wiki' }); }, [navigate]); /** * Opens the system folder selection dialog and initializes the selected project. */ const handleOpenFolder = useCallback(async () => { const api = getElectronAPI(); const result = await api.openDirectory(); if (!result.canceled && result.filePaths[0]) { const path = result.filePaths[0]; // Extract folder name from path (works on both Windows and Mac/Linux) const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project'; try { // Check if this is a brand new project (no .automaker directory) const hadAutomakerDir = await hasAutomakerDir(path); // Initialize the .automaker directory structure const initResult = await initializeProject(path); if (!initResult.success) { toast.error('Failed to initialize project', { description: initResult.error || 'Unknown error occurred', }); return; } // Upsert project and set as current (handles both create and update cases) // Theme preservation is handled by the store action const trashedProject = trashedProjects.find((p) => p.path === path); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; upsertAndSetCurrentProject(path, name, effectiveTheme); // Check if app_spec.txt exists const specExists = await hasAppSpec(path); if (!hadAutomakerDir && !specExists) { // This is a brand new project - show setup dialog setSetupProjectPath(path); setShowSetupDialog(true); toast.success('Project opened', { description: `Opened ${name}. Let's set up your app specification!`, }); } else if (initResult.createdFiles && initResult.createdFiles.length > 0) { toast.success(initResult.isNewProject ? 'Project initialized' : 'Project updated', { description: `Set up ${initResult.createdFiles.length} file(s) in .automaker`, }); } else { toast.success('Project opened', { description: `Opened ${name}`, }); } // Navigate to board view navigate({ to: '/board' }); } catch (error) { console.error('Failed to open project:', error); toast.error('Failed to open project', { description: error instanceof Error ? error.message : 'Unknown error', }); } } }, [trashedProjects, upsertAndSetCurrentProject, currentProject, globalTheme, navigate]); // Handler for creating initial spec from the setup dialog const handleCreateInitialSpec = useCallback(async () => { if (!setupProjectPath) return; setSpecCreatingForProject(setupProjectPath); setShowSetupDialog(false); try { const api = getElectronAPI(); if (!api.specRegeneration) { toast.error('Spec regeneration not available'); setSpecCreatingForProject(null); return; } await api.specRegeneration.create( setupProjectPath, projectOverview, generateFeatures, analyzeProject, featureCount ); } catch (error) { console.error('Failed to generate spec:', error); toast.error('Failed to generate spec', { description: error instanceof Error ? error.message : 'Unknown error', }); setSpecCreatingForProject(null); } }, [ setupProjectPath, projectOverview, generateFeatures, analyzeProject, featureCount, setSpecCreatingForProject, ]); const handleSkipSetup = useCallback(() => { setShowSetupDialog(false); setSetupProjectPath(null); }, []); // Keyboard shortcuts for project switching (1-9, 0) useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { // Ignore if user is typing in an input, textarea, or contenteditable const target = event.target as HTMLElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return; } // Ignore if modifier keys are pressed (except for standalone number keys) if (event.ctrlKey || event.metaKey || event.altKey) { return; } // Map key to project index: "1" -> 0, "2" -> 1, ..., "9" -> 8, "0" -> 9 const key = event.key; let projectIndex: number | null = null; if (key >= '1' && key <= '9') { projectIndex = parseInt(key, 10) - 1; // "1" -> 0, "9" -> 8 } else if (key === '0') { projectIndex = 9; // "0" -> 9 } if (projectIndex !== null && projectIndex < projects.length) { const targetProject = projects[projectIndex]; if (targetProject && targetProject.id !== currentProject?.id) { handleProjectClick(targetProject); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [projects, currentProject, handleProjectClick]); return ( <> {/* Context Menu */} {contextMenuProject && contextMenuPosition && ( )} {/* Edit Project Dialog */} {editDialogProject && ( !open && setEditDialogProject(null)} /> )} {/* New Project Modal */} {/* Onboarding Dialog */} {/* Setup Dialog for Open Project */} ); }