import React, { useState, useEffect } from 'react'; import { X, FolderPlus, GitBranch, Key, ChevronRight, ChevronLeft, Check, Loader2, AlertCircle, FolderOpen, Eye, EyeOff, Plus } from 'lucide-react'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { api } from '../utils/api'; import { useTranslation } from 'react-i18next'; const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { const { t } = useTranslation(); // Wizard state const [step, setStep] = useState(1); // 1: Choose type, 2: Configure, 3: Confirm const [workspaceType, setWorkspaceType] = useState('existing'); // 'existing' or 'new' - default to 'existing' // Form state const [workspacePath, setWorkspacePath] = useState(''); const [githubUrl, setGithubUrl] = useState(''); const [selectedGithubToken, setSelectedGithubToken] = useState(''); const [tokenMode, setTokenMode] = useState('stored'); // 'stored' | 'new' | 'none' const [newGithubToken, setNewGithubToken] = useState(''); // UI state const [isCreating, setIsCreating] = useState(false); const [error, setError] = useState(null); const [availableTokens, setAvailableTokens] = useState([]); const [loadingTokens, setLoadingTokens] = useState(false); const [pathSuggestions, setPathSuggestions] = useState([]); const [showPathDropdown, setShowPathDropdown] = useState(false); const [showFolderBrowser, setShowFolderBrowser] = useState(false); const [browserCurrentPath, setBrowserCurrentPath] = useState('~'); const [browserFolders, setBrowserFolders] = useState([]); const [loadingFolders, setLoadingFolders] = useState(false); const [showHiddenFolders, setShowHiddenFolders] = useState(false); const [showNewFolderInput, setShowNewFolderInput] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [creatingFolder, setCreatingFolder] = useState(false); const [cloneProgress, setCloneProgress] = useState(''); // Load available GitHub tokens when needed useEffect(() => { if (step === 2 && workspaceType === 'new' && githubUrl) { loadGithubTokens(); } }, [step, workspaceType, githubUrl]); // Load path suggestions useEffect(() => { if (workspacePath.length > 2) { loadPathSuggestions(workspacePath); } else { setPathSuggestions([]); setShowPathDropdown(false); } }, [workspacePath]); const loadGithubTokens = async () => { try { setLoadingTokens(true); const response = await api.get('/settings/credentials?type=github_token'); const data = await response.json(); const activeTokens = (data.credentials || []).filter(t => t.is_active); setAvailableTokens(activeTokens); // Auto-select first token if available if (activeTokens.length > 0 && !selectedGithubToken) { setSelectedGithubToken(activeTokens[0].id.toString()); } } catch (error) { console.error('Error loading GitHub tokens:', error); } finally { setLoadingTokens(false); } }; const loadPathSuggestions = async (inputPath) => { try { // Extract the directory to browse (parent of input) const lastSlash = inputPath.lastIndexOf('/'); const dirPath = lastSlash > 0 ? inputPath.substring(0, lastSlash) : '~'; const response = await api.browseFilesystem(dirPath); const data = await response.json(); if (data.suggestions) { // Filter suggestions based on the input, excluding exact match const filtered = data.suggestions.filter(s => s.path.toLowerCase().startsWith(inputPath.toLowerCase()) && s.path.toLowerCase() !== inputPath.toLowerCase() ); setPathSuggestions(filtered.slice(0, 5)); setShowPathDropdown(filtered.length > 0); } } catch (error) { console.error('Error loading path suggestions:', error); } }; const handleNext = () => { setError(null); if (step === 1) { if (!workspaceType) { setError(t('projectWizard.errors.selectType')); return; } setStep(2); } else if (step === 2) { if (!workspacePath.trim()) { setError(t('projectWizard.errors.providePath')); return; } // No validation for GitHub token - it's optional (only needed for private repos) setStep(3); } }; const handleBack = () => { setError(null); setStep(step - 1); }; const handleCreate = async () => { setIsCreating(true); setError(null); setCloneProgress(''); try { if (workspaceType === 'new' && githubUrl) { const params = new URLSearchParams({ path: workspacePath.trim(), githubUrl: githubUrl.trim(), }); if (tokenMode === 'stored' && selectedGithubToken) { params.append('githubTokenId', selectedGithubToken); } else if (tokenMode === 'new' && newGithubToken) { params.append('newGithubToken', newGithubToken.trim()); } const token = localStorage.getItem('auth-token'); const url = `/api/projects/clone-progress?${params}${token ? `&token=${token}` : ''}`; await new Promise((resolve, reject) => { const eventSource = new EventSource(url); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'progress') { setCloneProgress(data.message); } else if (data.type === 'complete') { eventSource.close(); if (onProjectCreated) { onProjectCreated(data.project); } onClose(); resolve(); } else if (data.type === 'error') { eventSource.close(); reject(new Error(data.message)); } } catch (e) { console.error('Error parsing SSE event:', e); } }; eventSource.onerror = () => { eventSource.close(); reject(new Error('Connection lost during clone')); }; }); return; } const payload = { workspaceType, path: workspacePath.trim(), }; const response = await api.createWorkspace(payload); const data = await response.json(); if (!response.ok) { throw new Error(data.error || t('projectWizard.errors.failedToCreate')); } if (onProjectCreated) { onProjectCreated(data.project); } onClose(); } catch (error) { console.error('Error creating workspace:', error); setError(error.message || t('projectWizard.errors.failedToCreate')); } finally { setIsCreating(false); } }; const selectPathSuggestion = (suggestion) => { setWorkspacePath(suggestion.path); setShowPathDropdown(false); }; const openFolderBrowser = async () => { setShowFolderBrowser(true); await loadBrowserFolders('~'); }; const loadBrowserFolders = async (path) => { try { setLoadingFolders(true); const response = await api.browseFilesystem(path); const data = await response.json(); setBrowserCurrentPath(data.path || path); setBrowserFolders(data.suggestions || []); } catch (error) { console.error('Error loading folders:', error); } finally { setLoadingFolders(false); } }; const selectFolder = (folderPath, advanceToConfirm = false) => { setWorkspacePath(folderPath); setShowFolderBrowser(false); if (advanceToConfirm) { setStep(3); } }; const navigateToFolder = async (folderPath) => { await loadBrowserFolders(folderPath); }; const createNewFolder = async () => { if (!newFolderName.trim()) return; setCreatingFolder(true); setError(null); try { const separator = browserCurrentPath.includes('\\') ? '\\' : '/'; const folderPath = `${browserCurrentPath}${separator}${newFolderName.trim()}`; const response = await api.createFolder(folderPath); const data = await response.json(); if (!response.ok) { throw new Error(data.error || t('projectWizard.errors.failedToCreateFolder', 'Failed to create folder')); } setNewFolderName(''); setShowNewFolderInput(false); await loadBrowserFolders(data.path || folderPath); } catch (error) { console.error('Error creating folder:', error); setError(error.message || t('projectWizard.errors.failedToCreateFolder', 'Failed to create folder')); } finally { setCreatingFolder(false); } }; return (
{/* Header */}

{t('projectWizard.title')}

{/* Progress Indicator */}
{[1, 2, 3].map((s) => (
{s < step ? : s}
{s === 1 ? t('projectWizard.steps.type') : s === 2 ? t('projectWizard.steps.configure') : t('projectWizard.steps.confirm')}
{s < 3 && (
)} ))}
{/* Content */}
{/* Error Display */} {error && (

{error}

)} {/* Step 1: Choose workspace type */} {step === 1 && (

{t('projectWizard.step1.question')}

{/* Existing Workspace */} {/* New Workspace */}
)} {/* Step 2: Configure workspace */} {step === 2 && (
{/* Workspace Path */}
setWorkspacePath(e.target.value)} placeholder={workspaceType === 'existing' ? '/path/to/existing/workspace' : '/path/to/new/workspace'} className="w-full" /> {showPathDropdown && pathSuggestions.length > 0 && (
{pathSuggestions.map((suggestion, index) => ( ))}
)}

{workspaceType === 'existing' ? t('projectWizard.step2.existingHelp') : t('projectWizard.step2.newHelp')}

{/* GitHub URL (only for new workspace) */} {workspaceType === 'new' && ( <>
setGithubUrl(e.target.value)} placeholder="https://github.com/username/repository" className="w-full" />

{t('projectWizard.step2.githubHelp')}

{/* GitHub Token (only for HTTPS URLs - SSH uses SSH keys) */} {githubUrl && !githubUrl.startsWith('git@') && !githubUrl.startsWith('ssh://') && (
{t('projectWizard.step2.githubAuth')}

{t('projectWizard.step2.githubAuthHelp')}

{loadingTokens ? (
{t('projectWizard.step2.loadingTokens')}
) : availableTokens.length > 0 ? ( <> {/* Token Selection Tabs */}
{tokenMode === 'stored' ? (
) : tokenMode === 'new' ? (
setNewGithubToken(e.target.value)} placeholder="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" className="w-full" />

{t('projectWizard.step2.tokenHelp')}

) : null} ) : (

{t('projectWizard.step2.publicRepoInfo')}

setNewGithubToken(e.target.value)} placeholder={t('projectWizard.step2.tokenPublicPlaceholder')} className="w-full" />

{t('projectWizard.step2.noTokensHelp')}

)}
)} )}
)} {/* Step 3: Confirm */} {step === 3 && (

{t('projectWizard.step3.reviewConfig')}

{t('projectWizard.step3.workspaceType')} {workspaceType === 'existing' ? t('projectWizard.step3.existingWorkspace') : t('projectWizard.step3.newWorkspace')}
{t('projectWizard.step3.path')} {workspacePath}
{workspaceType === 'new' && githubUrl && ( <>
{t('projectWizard.step3.cloneFrom')} {githubUrl}
{t('projectWizard.step3.authentication')} {tokenMode === 'stored' && selectedGithubToken ? `${t('projectWizard.step3.usingStoredToken')} ${availableTokens.find(t => t.id.toString() === selectedGithubToken)?.credential_name || 'Unknown'}` : tokenMode === 'new' && newGithubToken ? t('projectWizard.step3.usingProvidedToken') : (githubUrl.startsWith('git@') || githubUrl.startsWith('ssh://')) ? t('projectWizard.step3.sshKey', 'SSH Key') : t('projectWizard.step3.noAuthentication')}
)}
{isCreating && cloneProgress ? (

{t('projectWizard.step3.cloningRepository', 'Cloning repository...')}

{cloneProgress}
) : (

{workspaceType === 'existing' ? t('projectWizard.step3.existingInfo') : githubUrl ? t('projectWizard.step3.newWithClone') : t('projectWizard.step3.newEmpty')}

)}
)}
{/* Footer */}
{/* Folder Browser Modal */} {showFolderBrowser && (
{/* Browser Header */}

Select Folder

{/* New Folder Input */} {showNewFolderInput && (
setNewFolderName(e.target.value)} placeholder="New folder name" className="flex-1" onKeyDown={(e) => { if (e.key === 'Enter') createNewFolder(); if (e.key === 'Escape') { setShowNewFolderInput(false); setNewFolderName(''); } }} autoFocus />
)} {/* Folder List */}
{loadingFolders ? (
) : (
{/* Parent Directory - check for Windows root (e.g., C:\) and Unix root */} {browserCurrentPath !== '~' && browserCurrentPath !== '/' && !/^[A-Za-z]:\\?$/.test(browserCurrentPath) && ( )} {/* Folders */} {browserFolders.length === 0 ? (
No subfolders found
) : ( browserFolders .filter(folder => showHiddenFolders || !folder.name.startsWith('.')) .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) .map((folder, index) => (
)) )}
)}
{/* Browser Footer with Current Path */}
Path: {browserCurrentPath}
)}
); }; export default ProjectCreationWizard;