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 (
{error}
{workspaceType === 'existing' ? t('projectWizard.step2.existingHelp') : t('projectWizard.step2.newHelp')}
{t('projectWizard.step2.githubHelp')}
{t('projectWizard.step2.githubAuthHelp')}
{t('projectWizard.step2.tokenHelp')}
{t('projectWizard.step2.publicRepoInfo')}
{t('projectWizard.step2.noTokensHelp')}
{t('projectWizard.step3.cloningRepository', 'Cloning repository...')}
{cloneProgress}
{workspaceType === 'existing' ? t('projectWizard.step3.existingInfo') : githubUrl ? t('projectWizard.step3.newWithClone') : t('projectWizard.step3.newEmpty')}
)}
{browserCurrentPath}