Files
automaker/apps/app/src/components/views/analysis-view.tsx
Cody Seibert 9bb843f82f chore: update dependencies and improve project structure
- Added `morgan` for enhanced request logging in the server.
- Updated `package-lock.json` to include new dependencies and their types.
- Refactored the `NewProjectModal` component for improved readability and structure.
- Enhanced the `FileBrowserDialog` to support initial path selection and improved error handling.
- Updated various components to ensure consistent formatting and better user experience.
- Introduced XML format specification for app specifications to maintain consistency across the application.
2025-12-14 10:59:52 -05:00

1135 lines
36 KiB
TypeScript

"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<Set<string>>(
new Set()
);
const [isGeneratingSpec, setIsGeneratingSpec] = useState(false);
const [specGenerated, setSpecGenerated] = useState(false);
const [specError, setSpecError] = useState<string | null>(null);
const [isGeneratingFeatureList, setIsGeneratingFeatureList] = useState(false);
const [featureListGenerated, setFeatureListGenerated] = useState(false);
const [featureListError, setFeatureListError] = useState<string | null>(null);
// Recursively scan directory
const scanDirectory = useCallback(
async (path: string, depth: number = 0): Promise<FileTreeNode[]> => {
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<string, number> } => {
let files = 0;
let dirs = 0;
const byExt: Record<string, number> = {};
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<string, string> = {};
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(` <directory name="${dir}" />`);
}
return structure.join("\n");
};
const projectName = getProjectName();
const description = getProjectDescription();
const techStack = detectTechStack();
// Generate the spec content
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
const specContent = `<project_specification>
<project_name>${projectName}</project_name>
<overview>
${description}
</overview>
<technology_stack>
<languages>
${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]) =>
` <language ext=".${ext}" count="${count}" />`
)
.join("\n")}
</languages>
<frameworks>
${techStack.map((tech) => ` <framework>${tech}</framework>`).join("\n")}
</frameworks>
</technology_stack>
<project_structure>
<total_files>${projectAnalysis.totalFiles}</total_files>
<total_directories>${projectAnalysis.totalDirectories}</total_directories>
<top_level_structure>
${analyzeStructure()}
</top_level_structure>
</project_structure>
<file_breakdown>
${Object.entries(projectAnalysis.filesByExtension)
.sort((a: [string, number], b: [string, number]) => b[1] - a[1])
.slice(0, 10)
.map(
([ext, count]: [string, number]) =>
` <extension type="${
ext.startsWith("(") ? ext : "." + ext
}" count="${count}" />`
)
.join("\n")}
</file_breakdown>
<analyzed_at>${projectAnalysis.analyzedAt}</analyzed_at>
</project_specification>
`;
// 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<string, string> = {};
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 (
<div key={node.path} data-testid={`analysis-node-${node.name}`}>
<div
className={cn(
"flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-muted/50 text-sm"
)}
style={{ paddingLeft: `${depth * 16 + 8}px` }}
onClick={() => {
if (node.isDirectory) {
toggleFolder(node.path);
}
}}
>
{node.isDirectory ? (
<>
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
) : (
<ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
)}
{isExpanded ? (
<FolderOpen className="w-4 h-4 text-primary shrink-0" />
) : (
<Folder className="w-4 h-4 text-primary shrink-0" />
)}
</>
) : (
<>
<span className="w-4" />
<File className="w-4 h-4 text-muted-foreground shrink-0" />
</>
)}
<span className="truncate">{node.name}</span>
{node.extension && (
<span className="text-xs text-muted-foreground ml-auto">
.{node.extension}
</span>
)}
</div>
{node.isDirectory && isExpanded && node.children && (
<div>
{node.children.map((child: FileTreeNode) =>
renderNode(child, depth + 1)
)}
</div>
)}
</div>
);
};
if (!currentProject) {
return (
<div
className="flex-1 flex items-center justify-center"
data-testid="analysis-view-no-project"
>
<p className="text-muted-foreground">No project selected</p>
</div>
);
}
return (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="analysis-view"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-zinc-950/50 backdrop-blur-md">
<div className="flex items-center gap-3">
<Search className="w-5 h-5 text-muted-foreground" />
<div>
<h1 className="text-xl font-bold">Project Analysis</h1>
<p className="text-sm text-muted-foreground">
{currentProject.name}
</p>
</div>
</div>
<Button
onClick={runAnalysis}
disabled={isAnalyzing}
data-testid="analyze-project-button"
>
{isAnalyzing ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Analyzing...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Analyze Project
</>
)}
</Button>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden p-4">
{!projectAnalysis && !isAnalyzing ? (
<div className="flex flex-col items-center justify-center h-full text-center">
<Search className="w-16 h-16 text-muted-foreground/50 mb-4" />
<h2 className="text-lg font-semibold mb-2">No Analysis Yet</h2>
<p className="text-sm text-muted-foreground mb-4 max-w-md">
Click &quot;Analyze Project&quot; to scan your codebase and get
insights about its structure.
</p>
<Button
onClick={runAnalysis}
data-testid="analyze-project-button-empty"
>
<Search className="w-4 h-4 mr-2" />
Start Analysis
</Button>
</div>
) : isAnalyzing ? (
<div className="flex flex-col items-center justify-center h-full">
<Loader2 className="w-12 h-12 animate-spin text-primary mb-4" />
<p className="text-muted-foreground">Scanning project files...</p>
</div>
) : projectAnalysis ? (
<div className="flex gap-4 h-full overflow-hidden">
{/* Stats Panel */}
<div className="w-80 shrink-0 overflow-y-auto space-y-4">
<Card data-testid="analysis-stats">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<BarChart3 className="w-4 h-4" />
Statistics
</CardTitle>
<CardDescription>
Analyzed{" "}
{new Date(projectAnalysis.analyzedAt).toLocaleString()}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">
Total Files
</span>
<span className="font-medium" data-testid="total-files">
{projectAnalysis.totalFiles}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">
Total Directories
</span>
<span
className="font-medium"
data-testid="total-directories"
>
{projectAnalysis.totalDirectories}
</span>
</div>
</CardContent>
</Card>
<Card data-testid="files-by-extension">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<FileCode className="w-4 h-4" />
Files by Extension
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{Object.entries(projectAnalysis.filesByExtension)
.sort(
(a: [string, number], b: [string, number]) =>
b[1] - a[1]
)
.slice(0, 15)
.map(([ext, count]: [string, number]) => (
<div key={ext} className="flex justify-between text-sm">
<span className="text-muted-foreground font-mono">
{ext.startsWith("(") ? ext : `.${ext}`}
</span>
<span>{count}</span>
</div>
))}
</div>
</CardContent>
</Card>
{/* Generate Spec Card */}
<Card data-testid="generate-spec-card">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<FileText className="w-4 h-4" />
Generate Specification
</CardTitle>
<CardDescription>
Create app_spec.txt from analysis
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">
Generate a project specification file based on the analyzed
codebase structure and detected technologies.
</p>
<Button
onClick={generateSpec}
disabled={isGeneratingSpec}
className="w-full"
data-testid="generate-spec-button"
>
{isGeneratingSpec ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<FileText className="w-4 h-4 mr-2" />
Generate Spec
</>
)}
</Button>
{specGenerated && (
<div
className="flex items-center gap-2 text-sm text-green-500"
data-testid="spec-generated-success"
>
<CheckCircle className="w-4 h-4" />
<span>app_spec.txt created successfully!</span>
</div>
)}
{specError && (
<div
className="flex items-center gap-2 text-sm text-red-500"
data-testid="spec-generated-error"
>
<AlertCircle className="w-4 h-4" />
<span>{specError}</span>
</div>
)}
</CardContent>
</Card>
{/* Generate Feature List Card */}
<Card data-testid="generate-feature-list-card">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<ListChecks className="w-4 h-4" />
Generate Feature List
</CardTitle>
<CardDescription>
Create features from analysis
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">
Automatically detect and generate a feature list based on
the analyzed codebase structure, dependencies, and project
configuration.
</p>
<Button
onClick={generateFeatureList}
disabled={isGeneratingFeatureList}
className="w-full"
data-testid="generate-feature-list-button"
>
{isGeneratingFeatureList ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<ListChecks className="w-4 h-4 mr-2" />
Generate Feature List
</>
)}
</Button>
{featureListGenerated && (
<div
className="flex items-center gap-2 text-sm text-green-500"
data-testid="feature-list-generated-success"
>
<CheckCircle className="w-4 h-4" />
<span>Features created successfully!</span>
</div>
)}
{featureListError && (
<div
className="flex items-center gap-2 text-sm text-red-500"
data-testid="feature-list-generated-error"
>
<AlertCircle className="w-4 h-4" />
<span>{featureListError}</span>
</div>
)}
</CardContent>
</Card>
</div>
{/* File Tree */}
<Card className="flex-1 overflow-hidden">
<CardHeader className="pb-2 border-b">
<CardTitle className="text-sm flex items-center gap-2">
<Folder className="w-4 h-4" />
File Tree
</CardTitle>
<CardDescription>
{projectAnalysis.totalFiles} files in{" "}
{projectAnalysis.totalDirectories} directories
</CardDescription>
</CardHeader>
<CardContent
className="p-0 overflow-y-auto h-full"
data-testid="analysis-file-tree"
>
<div className="p-2">
{projectAnalysis.fileTree.map((node: FileTreeNode) =>
renderNode(node)
)}
</div>
</CardContent>
</Card>
</div>
) : null}
</div>
</div>
);
}