diff --git a/PRD.md b/PRD.md index 4753eeb..b1c2ed3 100644 --- a/PRD.md +++ b/PRD.md @@ -68,6 +68,20 @@ This is a full-featured low-code IDE with multiple integrated tools (code editor - **Progression**: Create test suite manually or with AI → Select test type (component/function/hook/integration) → Add test cases → Configure setup, assertions, and teardown → AI can generate complete test suites → Export test files for Vitest/React Testing Library - **Success criteria**: Can create test suites for different types; test cases have multiple assertions; setup/teardown code is optional; AI tests are comprehensive; generates valid Vitest test code +### Flask Backend Designer +- **Functionality**: Visual designer for Flask REST API with blueprint management, endpoint configuration, route parameters, authentication settings, and CORS configuration +- **Purpose**: Design Python Flask backends without writing Flask code manually, creating a complete full-stack application ecosystem +- **Trigger**: Opening the Flask API tab +- **Progression**: Create blueprint → Add endpoints with HTTP methods → Configure query/path parameters → Set authentication and CORS requirements → Generate complete Flask application with blueprints +- **Success criteria**: Can create blueprints with multiple endpoints; supports all HTTP methods (GET/POST/PUT/DELETE/PATCH); parameters are properly configured; generates valid Flask code with proper routing + +### Project Settings Designer +- **Functionality**: Configure Next.js settings, manage npm packages and dependencies, define npm scripts, and set package manager preferences +- **Purpose**: Comprehensive project configuration without manually editing package.json or config files +- **Trigger**: Opening the Settings tab +- **Progression**: Configure Next.js features (TypeScript, ESLint, Tailwind, App Router) → Add/edit npm packages → Define build scripts → Set package manager → Export complete package.json +- **Success criteria**: Next.js config options are properly applied; packages separated into dependencies and devDependencies; scripts are valid; supports npm/yarn/pnpm; generates valid package.json + ### Auto Error Detection & Repair - **Functionality**: Automated error detection and AI-powered code repair system that scans files for syntax, type, import, and lint errors - **Purpose**: Automatically identify and fix code errors without manual debugging, saving time and reducing bugs @@ -98,6 +112,12 @@ This is a full-featured low-code IDE with multiple integrated tools (code editor - **No Errors Found**: Show success state when error scan finds no issues - **Unrepairable Errors**: Display clear messages when AI cannot fix certain errors and suggest manual intervention - **Context-Dependent Errors**: Use related files as context for more accurate error repair +- **Empty Flask Blueprints**: Show empty state with guidance for creating first endpoint +- **Invalid Flask Routes**: Validate route paths and warn about conflicts or invalid syntax +- **Missing Required Parameters**: Highlight endpoints with incomplete parameter configurations +- **Duplicate Package Names**: Prevent adding the same npm package twice +- **Invalid Package Versions**: Validate semantic versioning format for npm packages +- **Conflicting Scripts**: Warn when npm script names conflict with built-in commands ## Design Direction The design should evoke a professional IDE environment while remaining approachable - think Visual Studio Code meets Figma. Clean panels, clear hierarchy, and purposeful use of space to avoid overwhelming users with options. @@ -156,9 +176,11 @@ Animations should feel responsive and purposeful - quick panel transitions (200m - Database (database icon) for models - Tree (tree-structure icon) for components - PaintBrush (paint-brush icon) for styling + - Flask (flask icon) for Flask backend API + - Gear (gear icon) for project settings - Play (play icon) for Playwright E2E tests - BookOpen (book-open icon) for Storybook stories - - Flask (flask icon) for unit tests + - Cube (cube icon) for unit tests - Wrench (wrench icon) for error repair - FileCode (file-code icon) for individual files - Plus (plus icon) for create actions @@ -168,6 +190,8 @@ Animations should feel responsive and purposeful - quick panel transitions (200m - CheckCircle (check-circle icon) for success states - Warning (warning icon) for warnings - X (x icon) for errors + - Package (package icon) for npm packages + - Pencil (pencil icon) for edit actions - **Spacing**: - Panel padding: p-6 (24px) for main content areas diff --git a/src/App.tsx b/src/App.tsx index 87cc010..f7abc4a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,8 +6,8 @@ import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Card } from '@/components/ui/card' import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable' -import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench } from '@phosphor-icons/react' -import { ProjectFile, PrismaModel, ComponentNode, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest } from '@/types/project' +import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench, Gear, Cube } from '@phosphor-icons/react' +import { ProjectFile, PrismaModel, ComponentNode, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest, FlaskConfig, NextJsConfig, NpmSettings } from '@/types/project' import { CodeEditor } from '@/components/CodeEditor' import { ModelDesigner } from '@/components/ModelDesigner' import { ComponentTreeBuilder } from '@/components/ComponentTreeBuilder' @@ -16,8 +16,10 @@ import { FileExplorer } from '@/components/FileExplorer' import { PlaywrightDesigner } from '@/components/PlaywrightDesigner' import { StorybookDesigner } from '@/components/StorybookDesigner' import { UnitTestDesigner } from '@/components/UnitTestDesigner' +import { FlaskDesigner } from '@/components/FlaskDesigner' +import { ProjectSettingsDesigner } from '@/components/ProjectSettingsDesigner' import { ErrorPanel } from '@/components/ErrorPanel' -import { generateNextJSProject, generatePrismaSchema, generateMUITheme, generatePlaywrightTests, generateStorybookStories, generateUnitTests } from '@/lib/generators' +import { generateNextJSProject, generatePrismaSchema, generateMUITheme, generatePlaywrightTests, generateStorybookStories, generateUnitTests, generateFlaskApp } from '@/lib/generators' import { AIService } from '@/lib/ai-service' import { toast } from 'sonner' import { @@ -30,6 +32,43 @@ import { import { ScrollArea } from '@/components/ui/scroll-area' import { Textarea } from '@/components/ui/textarea' +const DEFAULT_FLASK_CONFIG: FlaskConfig = { + blueprints: [], + corsOrigins: ['http://localhost:3000'], + enableSwagger: true, + port: 5000, + debug: true, +} + +const DEFAULT_NEXTJS_CONFIG: NextJsConfig = { + appName: 'my-nextjs-app', + typescript: true, + eslint: true, + tailwind: true, + srcDirectory: true, + appRouter: true, + importAlias: '@/*', + turbopack: false, +} + +const DEFAULT_NPM_SETTINGS: NpmSettings = { + packages: [ + { id: '1', name: 'react', version: '^18.2.0', isDev: false }, + { id: '2', name: 'react-dom', version: '^18.2.0', isDev: false }, + { id: '3', name: 'next', version: '^14.0.0', isDev: false }, + { id: '4', name: '@mui/material', version: '^5.14.0', isDev: false }, + { id: '5', name: 'typescript', version: '^5.0.0', isDev: true }, + { id: '6', name: '@types/react', version: '^18.2.0', isDev: true }, + ], + scripts: { + dev: 'next dev', + build: 'next build', + start: 'next start', + lint: 'next lint', + }, + packageManager: 'npm', +} + const DEFAULT_THEME: ThemeConfig = { variants: [ { @@ -99,6 +138,9 @@ function App() { const [playwrightTests, setPlaywrightTests] = useKV('project-playwright-tests', []) const [storybookStories, setStorybookStories] = useKV('project-storybook-stories', []) const [unitTests, setUnitTests] = useKV('project-unit-tests', []) + const [flaskConfig, setFlaskConfig] = useKV('project-flask-config', DEFAULT_FLASK_CONFIG) + const [nextjsConfig, setNextjsConfig] = useKV('project-nextjs-config', DEFAULT_NEXTJS_CONFIG) + const [npmSettings, setNpmSettings] = useKV('project-npm-settings', DEFAULT_NPM_SETTINGS) const [activeFileId, setActiveFileId] = useState((files || [])[0]?.id || null) const [activeTab, setActiveTab] = useState('code') const [exportDialogOpen, setExportDialogOpen] = useState(false) @@ -111,6 +153,9 @@ function App() { const safePlaywrightTests = playwrightTests || [] const safeStorybookStories = storybookStories || [] const safeUnitTests = unitTests || [] + const safeFlaskConfig = flaskConfig || DEFAULT_FLASK_CONFIG + const safeNextjsConfig = nextjsConfig || DEFAULT_NEXTJS_CONFIG + const safeNpmSettings = npmSettings || DEFAULT_NPM_SETTINGS const { errors: autoDetectedErrors } = useAutoRepair(safeFiles, false) @@ -134,26 +179,51 @@ function App() { } const handleExportProject = () => { - const projectFiles = generateNextJSProject('my-nextjs-app', safeModels, safeComponents, safeTheme) + const projectFiles = generateNextJSProject(safeNextjsConfig.appName, safeModels, safeComponents, safeTheme) const prismaSchema = generatePrismaSchema(safeModels) const themeCode = generateMUITheme(safeTheme) const playwrightTestCode = generatePlaywrightTests(safePlaywrightTests) const storybookFiles = generateStorybookStories(safeStorybookStories) const unitTestFiles = generateUnitTests(safeUnitTests) + const flaskFiles = generateFlaskApp(safeFlaskConfig) - const allFiles = { + const packageJson = { + name: safeNextjsConfig.appName, + version: '0.1.0', + private: true, + scripts: safeNpmSettings.scripts, + dependencies: safeNpmSettings.packages + .filter(pkg => !pkg.isDev) + .reduce((acc, pkg) => { + acc[pkg.name] = pkg.version + return acc + }, {} as Record), + devDependencies: safeNpmSettings.packages + .filter(pkg => pkg.isDev) + .reduce((acc, pkg) => { + acc[pkg.name] = pkg.version + return acc + }, {} as Record), + } + + const allFiles: Record = { ...projectFiles, + 'package.json': JSON.stringify(packageJson, null, 2), 'prisma/schema.prisma': prismaSchema, 'src/theme.ts': themeCode, 'e2e/tests.spec.ts': playwrightTestCode, ...storybookFiles, ...unitTestFiles, - ...safeFiles.reduce((acc, file) => { - acc[file.path] = file.content - return acc - }, {} as Record), } + + Object.entries(flaskFiles).forEach(([path, content]) => { + allFiles[`backend/${path}`] = content + }) + + safeFiles.forEach(file => { + allFiles[file.path] = file.content + }) setGeneratedCode(allFiles) setExportDialogOpen(true) @@ -246,6 +316,14 @@ function App() { Styling + + + Flask API + + + + Settings + Playwright @@ -255,7 +333,7 @@ function App() { Storybook - + Unit Tests @@ -309,6 +387,19 @@ function App() { + + + + + + + + diff --git a/src/components/FlaskDesigner.tsx b/src/components/FlaskDesigner.tsx new file mode 100644 index 0000000..131770b --- /dev/null +++ b/src/components/FlaskDesigner.tsx @@ -0,0 +1,605 @@ +import { useState } from 'react' +import { FlaskBlueprint, FlaskEndpoint, FlaskParam, FlaskConfig } from '@/types/project' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Textarea } from '@/components/ui/textarea' +import { Switch } from '@/components/ui/switch' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Plus, Trash, Flask, Pencil } from '@phosphor-icons/react' +import { Badge } from '@/components/ui/badge' +import { Separator } from '@/components/ui/separator' + +interface FlaskDesignerProps { + config: FlaskConfig + onConfigChange: (config: FlaskConfig | ((current: FlaskConfig) => FlaskConfig)) => void +} + +export function FlaskDesigner({ config, onConfigChange }: FlaskDesignerProps) { + const [selectedBlueprintId, setSelectedBlueprintId] = useState( + config.blueprints[0]?.id || null + ) + const [blueprintDialogOpen, setBlueprintDialogOpen] = useState(false) + const [endpointDialogOpen, setEndpointDialogOpen] = useState(false) + const [editingBlueprint, setEditingBlueprint] = useState(null) + const [editingEndpoint, setEditingEndpoint] = useState(null) + + const selectedBlueprint = config.blueprints.find((b) => b.id === selectedBlueprintId) + + const handleAddBlueprint = () => { + setEditingBlueprint({ + id: `blueprint-${Date.now()}`, + name: '', + urlPrefix: '/', + endpoints: [], + description: '', + }) + setBlueprintDialogOpen(true) + } + + const handleEditBlueprint = (blueprint: FlaskBlueprint) => { + setEditingBlueprint({ ...blueprint }) + setBlueprintDialogOpen(true) + } + + const handleSaveBlueprint = () => { + if (!editingBlueprint) return + + onConfigChange((current) => { + const existingIndex = current.blueprints.findIndex((b) => b.id === editingBlueprint.id) + if (existingIndex >= 0) { + const updated = [...current.blueprints] + updated[existingIndex] = editingBlueprint + return { ...current, blueprints: updated } + } else { + return { ...current, blueprints: [...current.blueprints, editingBlueprint] } + } + }) + + setSelectedBlueprintId(editingBlueprint.id) + setBlueprintDialogOpen(false) + setEditingBlueprint(null) + } + + const handleDeleteBlueprint = (blueprintId: string) => { + onConfigChange((current) => ({ + ...current, + blueprints: current.blueprints.filter((b) => b.id !== blueprintId), + })) + if (selectedBlueprintId === blueprintId) { + setSelectedBlueprintId(null) + } + } + + const handleAddEndpoint = () => { + setEditingEndpoint({ + id: `endpoint-${Date.now()}`, + path: '/', + method: 'GET', + name: '', + description: '', + queryParams: [], + pathParams: [], + authentication: false, + corsEnabled: true, + }) + setEndpointDialogOpen(true) + } + + const handleEditEndpoint = (endpoint: FlaskEndpoint) => { + setEditingEndpoint({ ...endpoint }) + setEndpointDialogOpen(true) + } + + const handleSaveEndpoint = () => { + if (!editingEndpoint || !selectedBlueprintId) return + + onConfigChange((current) => { + const blueprints = [...current.blueprints] + const blueprintIndex = blueprints.findIndex((b) => b.id === selectedBlueprintId) + if (blueprintIndex >= 0) { + const blueprint = { ...blueprints[blueprintIndex] } + const endpointIndex = blueprint.endpoints.findIndex((e) => e.id === editingEndpoint.id) + + if (endpointIndex >= 0) { + blueprint.endpoints[endpointIndex] = editingEndpoint + } else { + blueprint.endpoints.push(editingEndpoint) + } + + blueprints[blueprintIndex] = blueprint + } + return { ...current, blueprints } + }) + + setEndpointDialogOpen(false) + setEditingEndpoint(null) + } + + const handleDeleteEndpoint = (endpointId: string) => { + if (!selectedBlueprintId) return + + onConfigChange((current) => { + const blueprints = [...current.blueprints] + const blueprintIndex = blueprints.findIndex((b) => b.id === selectedBlueprintId) + if (blueprintIndex >= 0) { + blueprints[blueprintIndex] = { + ...blueprints[blueprintIndex], + endpoints: blueprints[blueprintIndex].endpoints.filter((e) => e.id !== endpointId), + } + } + return { ...current, blueprints } + }) + } + + const addQueryParam = () => { + if (!editingEndpoint) return + setEditingEndpoint({ + ...editingEndpoint, + queryParams: [ + ...(editingEndpoint.queryParams || []), + { id: `param-${Date.now()}`, name: '', type: 'string', required: false }, + ], + }) + } + + const removeQueryParam = (paramId: string) => { + if (!editingEndpoint) return + setEditingEndpoint({ + ...editingEndpoint, + queryParams: editingEndpoint.queryParams?.filter((p) => p.id !== paramId) || [], + }) + } + + const updateQueryParam = (paramId: string, updates: Partial) => { + if (!editingEndpoint) return + setEditingEndpoint({ + ...editingEndpoint, + queryParams: + editingEndpoint.queryParams?.map((p) => (p.id === paramId ? { ...p, ...updates } : p)) || [], + }) + } + + const getMethodColor = (method: string) => { + switch (method) { + case 'GET': + return 'bg-blue-500/10 text-blue-500 border-blue-500/30' + case 'POST': + return 'bg-green-500/10 text-green-500 border-green-500/30' + case 'PUT': + return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30' + case 'DELETE': + return 'bg-red-500/10 text-red-500 border-red-500/30' + case 'PATCH': + return 'bg-purple-500/10 text-purple-500 border-purple-500/30' + default: + return 'bg-muted text-muted-foreground' + } + } + + return ( +
+
+
+
+
+ +
+
+

Flask Backend Designer

+

+ Design REST API endpoints and blueprints +

+
+
+ +
+
+ +
+
+
+

Blueprints

+
+ +
+ {config.blueprints.map((blueprint) => ( +
setSelectedBlueprintId(blueprint.id)} + > +
+
+

{blueprint.name}

+

+ {blueprint.urlPrefix} +

+ + {blueprint.endpoints.length} endpoints + +
+
+ + +
+
+
+ ))} +
+
+
+ +
+ {selectedBlueprint ? ( + <> +
+
+
+

{selectedBlueprint.name}

+

+ {selectedBlueprint.description || 'No description'} +

+
+ +
+
+ + +
+ {selectedBlueprint.endpoints.map((endpoint) => ( + + +
+
+
+ + {endpoint.method} + + + {selectedBlueprint.urlPrefix} + {endpoint.path} + +
+ {endpoint.name} + {endpoint.description} +
+
+ + +
+
+
+ +
+ {endpoint.authentication && ( +
+ 🔒 Authentication Required +
+ )} + {endpoint.queryParams && endpoint.queryParams.length > 0 && ( +
+

Query Parameters:

+
+ {endpoint.queryParams.map((param) => ( +
+ {param.name} + + {param.type} + + {param.required && ( + + required + + )} +
+ ))} +
+
+ )} +
+
+
+ ))} + + {selectedBlueprint.endpoints.length === 0 && ( + +

No endpoints yet

+ +
+ )} +
+
+ + ) : ( +
+
+ +

Select a blueprint or create a new one

+
+
+ )} +
+
+ + + + + + {editingBlueprint?.name ? 'Edit Blueprint' : 'New Blueprint'} + + Configure your Flask blueprint + + {editingBlueprint && ( +
+
+ + + setEditingBlueprint({ ...editingBlueprint, name: e.target.value }) + } + placeholder="e.g., users, auth, products" + /> +
+
+ + + setEditingBlueprint({ ...editingBlueprint, urlPrefix: e.target.value }) + } + placeholder="/api/v1" + /> +
+
+ +