"use client"; import { useCallback, useState } from "react"; import { useAppStore, FileTreeNode, ProjectAnalysis, Feature, } from "@/store/app-store"; import { getElectronAPI } from "@/lib/electron"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Folder, FolderOpen, File, ChevronRight, ChevronDown, Search, RefreshCw, BarChart3, FileCode, Loader2, FileText, CheckCircle, AlertCircle, ListChecks, } from "lucide-react"; import { cn } from "@/lib/utils"; const IGNORE_PATTERNS = [ "node_modules", ".git", ".next", "dist", "build", ".DS_Store", "*.log", ".cache", "coverage", "__pycache__", ".pytest_cache", ".venv", "venv", ".env", ]; const shouldIgnore = (name: string) => { return IGNORE_PATTERNS.some((pattern) => { if (pattern.startsWith("*")) { return name.endsWith(pattern.slice(1)); } return name === pattern; }); }; const getExtension = (filename: string): string => { const parts = filename.split("."); return parts.length > 1 ? parts.pop() || "" : ""; }; export function AnalysisView() { const { currentProject, projectAnalysis, isAnalyzing, setProjectAnalysis, setIsAnalyzing, clearAnalysis, } = useAppStore(); const [expandedFolders, setExpandedFolders] = useState>( new Set() ); const [isGeneratingSpec, setIsGeneratingSpec] = useState(false); const [specGenerated, setSpecGenerated] = useState(false); const [specError, setSpecError] = useState(null); const [isGeneratingFeatureList, setIsGeneratingFeatureList] = useState(false); const [featureListGenerated, setFeatureListGenerated] = useState(false); const [featureListError, setFeatureListError] = useState(null); // Recursively scan directory const scanDirectory = useCallback( async (path: string, depth: number = 0): Promise => { if (depth > 10) return []; // Prevent infinite recursion const api = getElectronAPI(); try { const result = await api.readdir(path); if (!result.success || !result.entries) return []; const nodes: FileTreeNode[] = []; const entries = result.entries.filter((e) => !shouldIgnore(e.name)); for (const entry of entries) { const fullPath = `${path}/${entry.name}`; const node: FileTreeNode = { name: entry.name, path: fullPath, isDirectory: entry.isDirectory, extension: entry.isFile ? getExtension(entry.name) : undefined, }; if (entry.isDirectory) { // Recursively scan subdirectories node.children = await scanDirectory(fullPath, depth + 1); } nodes.push(node); } // Sort: directories first, then files alphabetically nodes.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); return nodes; } catch (error) { console.error("Failed to scan directory:", path, error); return []; } }, [] ); // Count files and directories const countNodes = ( nodes: FileTreeNode[] ): { files: number; dirs: number; byExt: Record } => { let files = 0; let dirs = 0; const byExt: Record = {}; const traverse = (items: FileTreeNode[]) => { for (const item of items) { if (item.isDirectory) { dirs++; if (item.children) traverse(item.children); } else { files++; if (item.extension) { byExt[item.extension] = (byExt[item.extension] || 0) + 1; } else { byExt["(no extension)"] = (byExt["(no extension)"] || 0) + 1; } } } }; traverse(nodes); return { files, dirs, byExt }; }; // Run the analysis const runAnalysis = useCallback(async () => { if (!currentProject) return; setIsAnalyzing(true); clearAnalysis(); try { const fileTree = await scanDirectory(currentProject.path); const counts = countNodes(fileTree); const analysis: ProjectAnalysis = { fileTree, totalFiles: counts.files, totalDirectories: counts.dirs, filesByExtension: counts.byExt, analyzedAt: new Date().toISOString(), }; setProjectAnalysis(analysis); } catch (error) { console.error("Analysis failed:", error); } finally { setIsAnalyzing(false); } }, [ currentProject, setIsAnalyzing, clearAnalysis, scanDirectory, setProjectAnalysis, ]); // Generate app_spec.txt from analysis const generateSpec = useCallback(async () => { if (!currentProject || !projectAnalysis) return; setIsGeneratingSpec(true); setSpecError(null); setSpecGenerated(false); try { const api = getElectronAPI(); // Read key files to understand the project better const fileContents: Record = {}; const keyFiles = ["package.json", "README.md", "tsconfig.json"]; // Collect file paths from analysis const collectFilePaths = ( nodes: FileTreeNode[], maxDepth: number = 3, currentDepth: number = 0 ): string[] => { const paths: string[] = []; for (const node of nodes) { if (!node.isDirectory) { paths.push(node.path); } else if (node.children && currentDepth < maxDepth) { paths.push( ...collectFilePaths(node.children, maxDepth, currentDepth + 1) ); } } return paths; }; const allFilePaths = collectFilePaths(projectAnalysis.fileTree); // Try to read key configuration files for (const keyFile of keyFiles) { const filePath = `${currentProject.path}/${keyFile}`; const exists = await api.exists(filePath); if (exists) { const result = await api.readFile(filePath); if (result.success && result.content) { fileContents[keyFile] = result.content; } } } // Detect project type and tech stack const detectTechStack = () => { const stack: string[] = []; const extensions = projectAnalysis.filesByExtension; // Check package.json for dependencies if (fileContents["package.json"]) { try { const pkg = JSON.parse(fileContents["package.json"]); if (pkg.dependencies?.react || pkg.dependencies?.["react-dom"]) stack.push("React"); if (pkg.dependencies?.next) stack.push("Next.js"); if (pkg.dependencies?.vue) stack.push("Vue"); if (pkg.dependencies?.angular) stack.push("Angular"); if (pkg.dependencies?.express) stack.push("Express"); if (pkg.dependencies?.electron) stack.push("Electron"); if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) stack.push("TypeScript"); if ( pkg.devDependencies?.tailwindcss || pkg.dependencies?.tailwindcss ) stack.push("Tailwind CSS"); if (pkg.devDependencies?.playwright || pkg.dependencies?.playwright) stack.push("Playwright"); if (pkg.devDependencies?.jest || pkg.dependencies?.jest) stack.push("Jest"); } catch { // Ignore JSON parse errors } } // Detect by file extensions if (extensions["ts"] || extensions["tsx"]) stack.push("TypeScript"); if (extensions["py"]) stack.push("Python"); if (extensions["go"]) stack.push("Go"); if (extensions["rs"]) stack.push("Rust"); if (extensions["java"]) stack.push("Java"); if (extensions["css"] || extensions["scss"] || extensions["sass"]) stack.push("CSS/SCSS"); // Remove duplicates return [...new Set(stack)]; }; // Get project name from package.json or folder name const getProjectName = () => { if (fileContents["package.json"]) { try { const pkg = JSON.parse(fileContents["package.json"]); if (pkg.name) return pkg.name; } catch { // Ignore JSON parse errors } } // Fall back to folder name return currentProject.name; }; // Get project description from package.json or README const getProjectDescription = () => { if (fileContents["package.json"]) { try { const pkg = JSON.parse(fileContents["package.json"]); if (pkg.description) return pkg.description; } catch { // Ignore JSON parse errors } } if (fileContents["README.md"]) { // Extract first paragraph from README const lines = fileContents["README.md"].split("\n"); for (const line of lines) { const trimmed = line.trim(); if ( trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("!") && trimmed.length > 20 ) { return trimmed.substring(0, 200); } } } return "A software project"; }; // Group files by directory for structure analysis const analyzeStructure = () => { const structure: string[] = []; const topLevelDirs = projectAnalysis.fileTree .filter((n: FileTreeNode) => n.isDirectory) .map((n: FileTreeNode) => n.name); for (const dir of topLevelDirs) { structure.push(` `); } return structure.join("\n"); }; const projectName = getProjectName(); const description = getProjectDescription(); const techStack = detectTechStack(); // Generate the spec content const specContent = ` ${projectName} ${description} ${Object.entries(projectAnalysis.filesByExtension) .filter(([ext]: [string, number]) => ["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "cpp", "c"].includes( ext ) ) .sort((a: [string, number], b: [string, number]) => b[1] - a[1]) .slice(0, 5) .map( ([ext, count]: [string, number]) => ` ` ) .join("\n")} ${techStack.map((tech) => ` ${tech}`).join("\n")} ${projectAnalysis.totalFiles} ${projectAnalysis.totalDirectories} ${analyzeStructure()} ${Object.entries(projectAnalysis.filesByExtension) .sort((a: [string, number], b: [string, number]) => b[1] - a[1]) .slice(0, 10) .map( ([ext, count]: [string, number]) => ` ` ) .join("\n")} ${projectAnalysis.analyzedAt} `; // Write the spec file const specPath = `${currentProject.path}/.automaker/app_spec.txt`; const writeResult = await api.writeFile(specPath, specContent); if (writeResult.success) { setSpecGenerated(true); } else { setSpecError(writeResult.error || "Failed to write spec file"); } } catch (error) { console.error("Failed to generate spec:", error); setSpecError( error instanceof Error ? error.message : "Failed to generate spec" ); } finally { setIsGeneratingSpec(false); } }, [currentProject, projectAnalysis]); // Generate features from analysis and save to .automaker/features folder const generateFeatureList = useCallback(async () => { if (!currentProject || !projectAnalysis) return; setIsGeneratingFeatureList(true); setFeatureListError(null); setFeatureListGenerated(false); try { const api = getElectronAPI(); // Read key files to understand the project const fileContents: Record = {}; const keyFiles = ["package.json", "README.md"]; // Try to read key configuration files for (const keyFile of keyFiles) { const filePath = `${currentProject.path}/${keyFile}`; const exists = await api.exists(filePath); if (exists) { const result = await api.readFile(filePath); if (result.success && result.content) { fileContents[keyFile] = result.content; } } } // Collect file paths from analysis const collectFilePaths = (nodes: FileTreeNode[]): string[] => { const paths: string[] = []; for (const node of nodes) { if (!node.isDirectory) { paths.push(node.path); } else if (node.children) { paths.push(...collectFilePaths(node.children)); } } return paths; }; const allFilePaths = collectFilePaths(projectAnalysis.fileTree); // Analyze directories and files to detect features interface DetectedFeature { category: string; description: string; steps: string[]; passes: boolean; } const detectedFeatures: DetectedFeature[] = []; // Detect features based on project structure and files const detectFeatures = () => { const extensions = projectAnalysis.filesByExtension; const topLevelDirs = projectAnalysis.fileTree .filter((n: FileTreeNode) => n.isDirectory) .map((n: FileTreeNode) => n.name.toLowerCase()); const topLevelFiles = projectAnalysis.fileTree .filter((n: FileTreeNode) => !n.isDirectory) .map((n: FileTreeNode) => n.name.toLowerCase()); // Check for test directories and files const hasTests = topLevelDirs.includes("tests") || topLevelDirs.includes("test") || topLevelDirs.includes("__tests__") || allFilePaths.some( (p) => p.includes(".spec.") || p.includes(".test.") ); if (hasTests) { detectedFeatures.push({ category: "Testing", description: "Automated test suite", steps: [ "Step 1: Tests directory exists", "Step 2: Test files are present", "Step 3: Run test suite", ], passes: true, }); } // Check for components directory (UI components) const hasComponents = topLevelDirs.includes("components") || allFilePaths.some((p) => p.toLowerCase().includes("/components/")); if (hasComponents) { detectedFeatures.push({ category: "UI/Design", description: "Component-based UI architecture", steps: [ "Step 1: Components directory exists", "Step 2: UI components are defined", "Step 3: Components are reusable", ], passes: true, }); } // Check for src directory (organized source code) if (topLevelDirs.includes("src")) { detectedFeatures.push({ category: "Project Structure", description: "Organized source code structure", steps: [ "Step 1: Source directory exists", "Step 2: Code is properly organized", "Step 3: Follows best practices", ], passes: true, }); } // Check package.json for dependencies and detect features if (fileContents["package.json"]) { try { const pkg = JSON.parse(fileContents["package.json"]); // React/Next.js app detection if (pkg.dependencies?.react || pkg.dependencies?.["react-dom"]) { detectedFeatures.push({ category: "Frontend", description: "React-based user interface", steps: [ "Step 1: React is installed", "Step 2: Components render correctly", "Step 3: State management works", ], passes: true, }); } if (pkg.dependencies?.next) { detectedFeatures.push({ category: "Framework", description: "Next.js framework integration", steps: [ "Step 1: Next.js is configured", "Step 2: Pages/routes are defined", "Step 3: Server-side rendering works", ], passes: true, }); } // TypeScript support if ( pkg.devDependencies?.typescript || pkg.dependencies?.typescript || extensions["ts"] || extensions["tsx"] ) { detectedFeatures.push({ category: "Developer Experience", description: "TypeScript type safety", steps: [ "Step 1: TypeScript is configured", "Step 2: Type definitions exist", "Step 3: Code compiles without errors", ], passes: true, }); } // Tailwind CSS if ( pkg.devDependencies?.tailwindcss || pkg.dependencies?.tailwindcss ) { detectedFeatures.push({ category: "UI/Design", description: "Tailwind CSS styling", steps: [ "Step 1: Tailwind is configured", "Step 2: Styles are applied", "Step 3: Responsive design works", ], passes: true, }); } // ESLint/Prettier (code quality) if (pkg.devDependencies?.eslint || pkg.devDependencies?.prettier) { detectedFeatures.push({ category: "Developer Experience", description: "Code quality tools", steps: [ "Step 1: Linter is configured", "Step 2: Code passes lint checks", "Step 3: Formatting is consistent", ], passes: true, }); } // Electron (desktop app) if (pkg.dependencies?.electron || pkg.devDependencies?.electron) { detectedFeatures.push({ category: "Platform", description: "Electron desktop application", steps: [ "Step 1: Electron is configured", "Step 2: Main process runs", "Step 3: Renderer process loads", ], passes: true, }); } // Playwright testing if ( pkg.devDependencies?.playwright || pkg.devDependencies?.["@playwright/test"] ) { detectedFeatures.push({ category: "Testing", description: "Playwright end-to-end testing", steps: [ "Step 1: Playwright is configured", "Step 2: E2E tests are defined", "Step 3: Tests pass successfully", ], passes: true, }); } } catch { // Ignore JSON parse errors } } // Check for documentation if ( topLevelFiles.includes("readme.md") || topLevelDirs.includes("docs") ) { detectedFeatures.push({ category: "Documentation", description: "Project documentation", steps: [ "Step 1: README exists", "Step 2: Documentation is comprehensive", "Step 3: Setup instructions are clear", ], passes: true, }); } // Check for CI/CD configuration const hasCICD = topLevelDirs.includes(".github") || topLevelFiles.includes(".gitlab-ci.yml") || topLevelFiles.includes(".travis.yml"); if (hasCICD) { detectedFeatures.push({ category: "DevOps", description: "CI/CD pipeline configuration", steps: [ "Step 1: CI config exists", "Step 2: Pipeline runs on push", "Step 3: Automated checks pass", ], passes: true, }); } // Check for API routes (Next.js API or Express) const hasAPIRoutes = allFilePaths.some( (p) => p.includes("/api/") || p.includes("/routes/") || p.includes("/endpoints/") ); if (hasAPIRoutes) { detectedFeatures.push({ category: "Backend", description: "API endpoints", steps: [ "Step 1: API routes are defined", "Step 2: Endpoints respond correctly", "Step 3: Error handling is implemented", ], passes: true, }); } // Check for state management const hasStateManagement = allFilePaths.some( (p) => p.includes("/store/") || p.includes("/stores/") || p.includes("/redux/") || p.includes("/context/") ); if (hasStateManagement) { detectedFeatures.push({ category: "Architecture", description: "State management system", steps: [ "Step 1: Store is configured", "Step 2: State updates correctly", "Step 3: Components access state", ], passes: true, }); } // Check for configuration files if ( topLevelFiles.includes("tsconfig.json") || topLevelFiles.includes("package.json") ) { detectedFeatures.push({ category: "Configuration", description: "Project configuration files", steps: [ "Step 1: Config files exist", "Step 2: Configuration is valid", "Step 3: Build process works", ], passes: true, }); } }; detectFeatures(); // If no features were detected, add a default feature if (detectedFeatures.length === 0) { detectedFeatures.push({ category: "Core", description: "Basic project structure", steps: [ "Step 1: Project directory exists", "Step 2: Files are present", "Step 3: Project can be loaded", ], passes: true, }); } // Create each feature using the features API if (!api.features) { throw new Error("Features API not available"); } for (const detectedFeature of detectedFeatures) { await api.features.create(currentProject.path, { id: crypto.randomUUID(), category: detectedFeature.category, description: detectedFeature.description, steps: detectedFeature.steps, status: "backlog", }); } setFeatureListGenerated(true); } catch (error) { console.error("Failed to generate feature list:", error); setFeatureListError( error instanceof Error ? error.message : "Failed to generate feature list" ); } finally { setIsGeneratingFeatureList(false); } }, [currentProject, projectAnalysis]); // Toggle folder expansion const toggleFolder = (path: string) => { const newExpanded = new Set(expandedFolders); if (expandedFolders.has(path)) { newExpanded.delete(path); } else { newExpanded.add(path); } setExpandedFolders(newExpanded); }; // Render file tree node const renderNode = (node: FileTreeNode, depth: number = 0) => { const isExpanded = expandedFolders.has(node.path); return (
{ if (node.isDirectory) { toggleFolder(node.path); } }} > {node.isDirectory ? ( <> {isExpanded ? ( ) : ( )} {isExpanded ? ( ) : ( )} ) : ( <> )} {node.name} {node.extension && ( .{node.extension} )}
{node.isDirectory && isExpanded && node.children && (
{node.children.map((child: FileTreeNode) => renderNode(child, depth + 1) )}
)}
); }; if (!currentProject) { return (

No project selected

); } return (
{/* Header */}

Project Analysis

{currentProject.name}

{/* Content */}
{!projectAnalysis && !isAnalyzing ? (

No Analysis Yet

Click "Analyze Project" to scan your codebase and get insights about its structure.

) : isAnalyzing ? (

Scanning project files...

) : projectAnalysis ? (
{/* Stats Panel */}
Statistics Analyzed{" "} {new Date(projectAnalysis.analyzedAt).toLocaleString()}
Total Files {projectAnalysis.totalFiles}
Total Directories {projectAnalysis.totalDirectories}
Files by Extension
{Object.entries(projectAnalysis.filesByExtension) .sort( (a: [string, number], b: [string, number]) => b[1] - a[1] ) .slice(0, 15) .map(([ext, count]: [string, number]) => (
{ext.startsWith("(") ? ext : `.${ext}`} {count}
))}
{/* Generate Spec Card */} Generate Specification Create app_spec.txt from analysis

Generate a project specification file based on the analyzed codebase structure and detected technologies.

{specGenerated && (
app_spec.txt created successfully!
)} {specError && (
{specError}
)}
{/* Generate Feature List Card */} Generate Feature List Create features from analysis

Automatically detect and generate a feature list based on the analyzed codebase structure, dependencies, and project configuration.

{featureListGenerated && (
Features created successfully!
)} {featureListError && (
{featureListError}
)}
{/* File Tree */} File Tree {projectAnalysis.totalFiles} files in{" "} {projectAnalysis.totalDirectories} directories
{projectAnalysis.fileTree.map((node: FileTreeNode) => renderNode(node) )}
) : null}
); }