[
  {
    "path": ".gitignore",
    "content": "node_modules/\ndist/\nbuild/\n.env\n.DS_Store\n*.log\n.vscode/\n.idea/\n\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [1.0.0] - Initial Release\n\n### Features\n- ✨ AI-powered image editing with natural language prompts\n- 🎨 Image generation from text descriptions\n- 🖼️ Dark theme UI with modern design\n- 📥 Image upload with validation\n- 💾 One-click download functionality\n- ⚡ Two modes: Edit and Generate\n\n### Backend Improvements\n- Removed unused imports (FormData)\n- Added comprehensive input validation\n- Implemented request timeout handling (120s)\n- Added file type and size validation\n- Improved error messages and logging\n- Added health check endpoint with API status\n- Structured error handling middleware\n- Added constants for configuration\n- Better API response handling\n\n### Frontend Improvements\n- Separated concerns into multiple files:\n  - `api.ts` - API client functions\n  - `types.ts` - TypeScript interfaces\n  - `utils.ts` - Helper functions\n- Added file validation before upload\n- Improved error handling and user feedback\n- Added API health check on startup\n- Added character counter for prompts\n- Added keyboard shortcuts (Ctrl/Cmd + Enter)\n- Better loading states and disabled states\n- Improved TypeScript type safety\n- Added warning messages for API configuration\n\n### Code Quality\n- Full TypeScript type coverage\n- Separated business logic from UI\n- Reusable utility functions\n- Better error propagation\n- Consistent code style\n- Comprehensive validation\n\n### Security\n- File type validation (JPEG, PNG, WebP only)\n- File size limits (50MB max)\n- Input sanitization\n- Prompt length limits (2000 chars)\n- Dimension validation for generated images\n\n### Documentation\n- Comprehensive README.md\n- Quick setup guide (SETUP.md)\n- API endpoint documentation\n- Troubleshooting section\n\n"
  },
  {
    "path": "DEVELOPMENT.md",
    "content": "# Development Guide\n\n## Project Overview\n\nNanoBanana Studio is a full-stack AI image editor powered by Google's nanobanana API via fal.ai. The project uses a modern tech stack with strong typing and comprehensive error handling.\n\n## Architecture\n\n### Backend (Node.js/Express)\n\n**File**: `backend/server.js`\n\n**Key Features**:\n- RESTful API endpoints for image editing and generation\n- Multer middleware for file uploads\n- Request validation and sanitization\n- Timeout handling (120s)\n- Comprehensive error handling\n- Health check endpoint\n\n**Endpoints**:\n- `GET /api/health` - Server health and API status\n- `POST /api/edit-image` - Edit images with AI\n- `POST /api/generate-image` - Generate images from text\n\n### Frontend (React/TypeScript/Vite)\n\n**File Structure**:\n- `App.tsx` - Main UI component\n- `api.ts` - API client functions\n- `types.ts` - TypeScript interfaces\n- `utils.ts` - Helper functions\n- `App.css` - Styles\n\n**Key Features**:\n- Type-safe API calls\n- Input validation\n- Error handling with user feedback\n- Keyboard shortcuts\n- Responsive UI\n\n## Development Workflow\n\n### 1. Initial Setup\n\n```bash\n# Install all dependencies\nnpm run install:all\n\n# Set up environment\ncd backend\ncp env.example .env\n# Edit .env and add FAL_API_KEY\n```\n\n### 2. Running Development Servers\n\n```bash\n# From root directory - runs both frontend and backend\nnpm run dev\n\n# Or run separately:\ncd frontend && npm run dev  # http://localhost:3000\ncd backend && npm run dev   # http://localhost:3001\n```\n\n### 3. Making Changes\n\n**Backend Changes**:\n- Edit `backend/server.js`\n- Server auto-restarts with `--watch` flag\n- Check console for errors\n\n**Frontend Changes**:\n- Edit files in `frontend/src/`\n- Vite hot-reloads changes automatically\n- Check browser console for errors\n\n## Code Style Guidelines\n\n### Backend\n\n```javascript\n// Use async/await for async operations\nasync function callAPI(params) {\n  try {\n    const response = await fetch(url, options);\n    return await response.json();\n  } catch (error) {\n    console.error('[CONTEXT] Error:', error.message);\n    throw error;\n  }\n}\n\n// Use constants for configuration\nconst MAX_FILE_SIZE = 50 * 1024 * 1024;\n\n// Validate inputs\nif (!prompt || !prompt.trim()) {\n  return res.status(400).json({ error: 'Prompt is required' });\n}\n```\n\n### Frontend\n\n```typescript\n// Define interfaces for all data structures\ninterface ApiResponse {\n  images?: Array<{ url: string }>;\n}\n\n// Use proper typing\nconst [state, setState] = useState<string | null>(null);\n\n// Extract reusable functions\nasync function handleApiCall() {\n  try {\n    const data = await apiFunction(params);\n    // Handle success\n  } catch (error) {\n    // Handle error\n  }\n}\n```\n\n## Testing\n\n### Manual Testing\n\n1. **Edit Mode**:\n   - Upload various image formats (JPEG, PNG, WebP)\n   - Test file size limits (try > 50MB)\n   - Test different prompts\n   - Test with/without negative prompts\n\n2. **Generate Mode**:\n   - Test various prompts\n   - Check image generation quality\n   - Test error scenarios\n\n3. **Error Cases**:\n   - Missing API key\n   - Invalid file types\n   - Network errors\n   - Timeout scenarios\n\n### API Testing\n\n```bash\n# Health check\ncurl http://localhost:3001/api/health\n\n# Generate image\ncurl -X POST http://localhost:3001/api/generate-image \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"prompt\": \"a beautiful sunset\"}'\n\n# Edit image (requires multipart/form-data)\ncurl -X POST http://localhost:3001/api/edit-image \\\n  -F \"image=@test.jpg\" \\\n  -F \"prompt=make it more dramatic\"\n```\n\n## Common Issues & Solutions\n\n### Issue: \"FAL_API_KEY not configured\"\n**Solution**: Create `backend/.env` file with valid API key\n\n### Issue: CORS errors\n**Solution**: Backend already has CORS enabled; check proxy configuration in `vite.config.ts`\n\n### Issue: File upload fails\n**Solution**: \n- Check file size (max 50MB)\n- Check file type (JPEG, PNG, WebP only)\n- Check browser console for detailed error\n\n### Issue: Timeout errors\n**Solution**: \n- Increase timeout in `backend/server.js` (API_TIMEOUT constant)\n- Check fal.ai API status\n- Reduce image size\n\n## Adding New Features\n\n### Adding a New API Endpoint\n\n1. Add endpoint to `backend/server.js`:\n```javascript\napp.post('/api/new-feature', async (req, res) => {\n  try {\n    // Validation\n    // API call\n    // Response\n  } catch (error) {\n    // Error handling\n  }\n});\n```\n\n2. Add API function to `frontend/src/api.ts`:\n```typescript\nexport async function newFeature(params: Params): Promise<Response> {\n  const response = await fetch('/api/new-feature', {\n    method: 'POST',\n    body: JSON.stringify(params),\n  });\n  return handleApiResponse(response);\n}\n```\n\n3. Use in component:\n```typescript\nconst handleNewFeature = async () => {\n  try {\n    const result = await newFeature(params);\n    // Handle result\n  } catch (error) {\n    // Handle error\n  }\n};\n```\n\n## Performance Optimization\n\n### Backend\n- Use response compression: `npm install compression`\n- Cache frequent API responses\n- Implement rate limiting\n\n### Frontend\n- Lazy load components\n- Optimize images before upload\n- Add request debouncing\n- Implement image caching\n\n## Deployment\n\n### Backend\n1. Build frontend: `cd frontend && npm run build`\n2. Set environment variables\n3. Start server: `cd backend && npm start`\n\n### Recommended Platforms\n- **Backend**: Railway, Render, Heroku\n- **Frontend**: Vercel, Netlify\n- **Full Stack**: Railway, Render\n\n### Environment Variables for Production\n```\nFAL_API_KEY=your_production_key\nPORT=3001\nNODE_ENV=production\n```\n\n## Resources\n\n- [fal.ai Documentation](https://fal.ai/models)\n- [React Documentation](https://react.dev)\n- [Express Documentation](https://expressjs.com)\n- [Vite Documentation](https://vitejs.dev)\n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 NanoBanana Studio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# NanoBanana Studio\n\nA modern, AI-powered image editor alternative to Photoshop/Photopea, powered by Google's nanobanana API from [fal.ai](https://fal.ai/dashboard).\n\n![NanoBanana Studio Screenshot](./screenshot.png)\n\n## Features\n\n- 🎨 **AI-Powered Image Editing**: Edit images using natural language prompts\n- ✨ **Image Generation**: Generate new images from text descriptions\n- 🖼️ **Intuitive UI**: Clean, modern interface inspired by professional image editors\n- ⚡ **Model Switcher**: Toggle between NanoBanana and [NanoBanana Pro](https://fal.ai/models/fal-ai/nano-banana-pro/edit/api) on the fly\n- 💾 **Easy Export**: Download your edited images with one click\n\n## Prerequisites\n\n- Node.js 18+ and npm\n- A fal.ai API key ([Get one here](https://fal.ai/dashboard/keys))\n\n## Installation\n\n1. Clone the repository and install dependencies:\n\n```bash\nnpm run install:all\n```\n\n2. Set up your environment variables:\n\n```bash\ncd backend\ncp env.example .env\n```\n\nEdit `backend/.env` and add your fal.ai API key:\n\n```\nFAL_API_KEY=your_fal_ai_api_key_here\nPORT=3001\n```\n\n**Note:** Get your fal.ai API key from [fal.ai](https://fal.ai). You'll need to sign up and create an API key in your dashboard.\n\n## Running the Application\n\nStart both frontend and backend in development mode:\n\n```bash\nnpm run dev\n```\n\n- Frontend will be available at: http://localhost:3000\n- Backend API will be available at: http://localhost:3001\n\n## Usage\n\n### Edit Mode\n\n1. Click \"Upload Image\" to select an image file\n2. Enter a natural language prompt describing the edits you want (e.g., \"make the sky more dramatic\", \"add a sunset\", \"remove the background\")\n3. Optionally add a negative prompt to exclude unwanted elements\n4. Click \"Edit Image\" and wait for processing\n5. Download your edited image\n\n## API Configuration\n\n**Important:** The application uses fal.ai's NanoBanana APIs:\n\n- Standard: [NanoBanana Edit](https://fal.ai/models/fal-ai/nano-banana/edit/api)\n- Pro tier: [NanoBanana Pro Edit](https://fal.ai/models/fal-ai/nano-banana-pro/edit/api)\n\nSelect your model in the UI, and the backend automatically routes to the appropriate endpoint. If fal.ai updates these URLs, adjust `MODEL_ENDPOINTS` in `backend/server.js`.\n\nTo verify the correct endpoint:\n1. Check the [fal.ai documentation](https://fal.ai/models)\n2. Look for the nanobanana model endpoint\n3. Update the fetch URL in `backend/server.js` if needed\n\n## API Endpoints\n\n### POST `/api/edit-image`\n\nEdit an existing image using AI.\n\n**Request:**\n- `image` (file): Image file to edit\n- `prompt` (string): Natural language editing instructions\n- `negativePrompt` (string, optional): Things to avoid in the edit\n\n**Response:**\n```json\n{\n  \"images\": [{\"url\": \"data:image/...\"}]\n}\n```\n\n### POST `/api/generate-image`\n\nGenerate a new image from text.\n\n**Request:**\n```json\n{\n  \"prompt\": \"a beautiful landscape...\",\n  \"negativePrompt\": \"blurry, low quality\",\n  \"width\": 1024,\n  \"height\": 1024\n}\n```\n\n**Response:**\n```json\n{\n  \"images\": [{\"url\": \"data:image/...\"}]\n}\n```\n\n## Tech Stack\n\n- **Frontend**: React + TypeScript + Vite\n- **Backend**: Node.js + Express\n- **AI**: Google nanobanana via fal.ai\n- **UI**: Custom CSS with modern design\n\n## Project Structure\n\n```\nnanobanana-studio/\n├── frontend/          # React frontend application\n│   ├── src/\n│   │   ├── App.tsx    # Main application component\n│   │   ├── api.ts     # API client functions\n│   │   ├── types.ts   # TypeScript type definitions\n│   │   ├── utils.ts   # Utility functions\n│   │   ├── App.css    # Styles\n│   │   └── ...\n│   └── package.json\n├── backend/           # Express backend server\n│   ├── server.js      # API server with validation & error handling\n│   ├── env.example    # Environment variables template\n│   └── package.json\n├── package.json       # Root package.json\n├── README.md          # Full documentation\n└── SETUP.md          # Quick setup guide\n```\n\n## Code Quality Features\n\n- **TypeScript**: Full type safety on frontend\n- **Error Handling**: Comprehensive error handling on both frontend and backend\n- **Validation**: Input validation for images, prompts, and parameters\n- **API Timeout**: 120-second timeout for API requests\n- **File Validation**: Type and size validation for uploaded images\n- **Logging**: Structured logging for debugging\n- **Security**: File type validation, size limits, and input sanitization\n\n## License\n\nMIT\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n"
  },
  {
    "path": "SETUP.md",
    "content": "# Quick Setup Guide\n\n## Step 1: Install Dependencies\n\n```bash\nnpm run install:all\n```\n\nThis will install dependencies for:\n- Root package (concurrently for running both servers)\n- Frontend (React + TypeScript + Vite)\n- Backend (Express + API dependencies)\n\n## Step 2: Configure API Key\n\n1. Get your fal.ai API key:\n   - Visit [fal.ai](https://fal.ai)\n   - Sign up or log in\n   - Navigate to your API keys section\n   - Create a new API key\n\n2. Set up environment variables:\n   ```bash\n   cd backend\n   cp env.example .env\n   ```\n\n3. Edit `backend/.env` and paste your API key:\n   ```\n   FAL_API_KEY=your_actual_api_key_here\n   PORT=3001\n   ```\n\n## Step 3: Run the Application\n\n```bash\nnpm run dev\n```\n\nThis starts:\n- Frontend on http://localhost:3000\n- Backend on http://localhost:3001\n\n## Step 4: Use the Application\n\n1. Open http://localhost:3000 in your browser\n2. **Edit Mode:**\n   - Click \"Upload Image\"\n   - Enter a prompt like \"make the sky more dramatic\"\n   - Click \"Edit Image\"\n   - Wait for processing\n   - Download your result\n\n3. **Generate Mode:**\n   - Switch to \"Generate\" mode\n   - Enter a prompt like \"a beautiful sunset over mountains\"\n   - Click \"Generate Image\"\n   - Download the generated image\n\n## Troubleshooting\n\n### \"FAL_API_KEY not configured\" error\n- Make sure you created the `.env` file in the `backend` directory\n- Verify the API key is correct (no extra spaces)\n- Restart the backend server after adding the key\n\n### API endpoint errors\n- Check that your fal.ai API key is valid\n- Verify the endpoint URL in `backend/server.js` matches fal.ai's current API\n- Check the browser console and server logs for detailed error messages\n\n### Port already in use\n- Change the PORT in `backend/.env` to a different number\n- Or stop the process using port 3000/3001\n\n## Next Steps\n\n- Customize the UI in `frontend/src/App.tsx` and `frontend/src/App.css`\n- Add more editing features\n- Implement image history/undo functionality\n- Add batch processing capabilities\n\n"
  },
  {
    "path": "backend/env.example",
    "content": "FAL_API_KEY=\nPORT=3001\n\n"
  },
  {
    "path": "backend/package.json",
    "content": "{\n  \"name\": \"nanobanana-backend\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"node --watch server.js\",\n    \"start\": \"node server.js\"\n  },\n  \"dependencies\": {\n    \"express\": \"^4.18.2\",\n    \"cors\": \"^2.8.5\",\n    \"dotenv\": \"^16.3.1\",\n    \"multer\": \"^1.4.5-lts.1\",\n    \"form-data\": \"^4.0.0\",\n    \"node-fetch\": \"^3.3.2\",\n    \"@fal-ai/client\": \"^1.0.0\"\n  }\n}\n\n"
  },
  {
    "path": "backend/server.js",
    "content": "import express from 'express';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport multer from 'multer';\nimport fetch from 'node-fetch';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\nimport { existsSync } from 'fs';\nimport { Buffer } from 'buffer';\nimport { fal } from '@fal-ai/client';\n\ndotenv.config();\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst app = express();\nconst PORT = process.env.PORT || 3001;\nconst FAL_API_KEY = process.env.FAL_API_KEY;\n\n// Configure fal.ai client\nif (FAL_API_KEY) {\n  fal.config({\n    credentials: FAL_API_KEY,\n  });\n}\n\n// Constants\nconst MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\nconst ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp'];\nconst API_TIMEOUT = 120000; // 120 seconds\nconst MODEL_ENDPOINTS = {\n  nano: 'fal-ai/nano-banana/edit',\n  pro: 'fal-ai/nano-banana-pro/edit',\n};\n\n// Middleware\napp.use(cors());\napp.use(express.json({ limit: '10mb' }));\n\n// Only serve static files in production (when dist folder exists)\nconst distPath = join(__dirname, '../frontend/dist');\nif (existsSync(distPath)) {\n  app.use(express.static(distPath));\n}\n\n// Configure multer for file uploads\nconst upload = multer({\n  storage: multer.memoryStorage(),\n  limits: { fileSize: MAX_FILE_SIZE },\n  fileFilter: (req, file, cb) => {\n    if (ALLOWED_MIME_TYPES.includes(file.mimetype)) {\n      cb(null, true);\n    } else {\n      cb(new Error('Invalid file type. Only JPEG, PNG, and WebP images are allowed.'));\n    }\n  }\n});\n\n// Utility function to call fal.ai API using the official client\nasync function callFalAPI(inputPayload, modelId = 'nano') {\n  if (!FAL_API_KEY) {\n    throw new Error('FAL_API_KEY not configured');\n  }\n\n  try {\n    const endpoint = MODEL_ENDPOINTS[modelId] || MODEL_ENDPOINTS.nano;\n    // Use fal.subscribe which handles queue polling automatically\n    const result = await fal.subscribe(endpoint, {\n      input: inputPayload,\n      logs: true,\n      onQueueUpdate: (update) => {\n        if (update.status === 'IN_PROGRESS') {\n          update.logs?.map((log) => log.message).forEach((msg) => {\n            console.log(`[FAL API] ${msg}`);\n          });\n        }\n      },\n    });\n\n    console.log(`[FAL API] Request completed, request_id: ${result.requestId}`);\n    // Return the data field from the result\n    return result.data;\n  } catch (error) {\n    console.error('[FAL API] Error:', error.message);\n    throw error;\n  }\n}\n\n// Validate and parse integer parameters\nfunction parseIntParam(value, defaultValue = undefined) {\n  if (!value) return defaultValue;\n  const parsed = parseInt(value, 10);\n  return isNaN(parsed) ? defaultValue : parsed;\n}\n\n// Health check\napp.get('/api/health', (req, res) => {\n  const isConfigured = !!FAL_API_KEY;\n  res.json({ \n    status: 'ok',\n    apiConfigured: isConfigured,\n    timestamp: new Date().toISOString()\n  });\n});\n\n// Image editing endpoint using fal.ai nanobanana\napp.post('/api/edit-image', upload.single('image'), async (req, res) => {\n  try {\n    // Validate image\n    if (!req.file) {\n      return res.status(400).json({ error: 'No image file provided' });\n    }\n\n    // Validate prompt\n    const { prompt, negativePrompt, seed, numInferenceSteps, model } = req.body;\n    if (!prompt || !prompt.trim()) {\n      return res.status(400).json({ error: 'Prompt is required' });\n    }\n\n    if (prompt.length > 2000) {\n      return res.status(400).json({ error: 'Prompt is too long (max 2000 characters)' });\n    }\n\n    // Convert image buffer to base64 data URL\n    const imageBase64 = req.file.buffer.toString('base64');\n    const imageDataUrl = `data:${req.file.mimetype};base64,${imageBase64}`;\n\n    // Prepare API payload according to fal.ai API schema\n    // API expects image_urls (array) and prompt\n    const payload = {\n      prompt: prompt.trim(),\n      image_urls: [imageDataUrl], // API expects array of image URLs\n    };\n\n    // Note: The nano-banana/edit API doesn't support negative_prompt, seed, or num_inference_steps\n    // These parameters are not in the API schema\n\n    const modelId = model === 'pro' ? 'pro' : 'nano';\n    console.log(`[EDIT] Processing image edit (${modelId}) with prompt: \"${prompt.substring(0, 50)}...\"`);\n\n    // Call fal.ai API\n    const data = await callFalAPI(payload, modelId);\n\n    console.log('[EDIT] Successfully processed image');\n    // API returns { images: [{ url, ... }], description: \"...\" }\n    // Frontend expects this format, so return as-is\n    res.json(data);\n  } catch (error) {\n    console.error('[EDIT] Error processing image:', error.message);\n    \n    const statusCode = error.message.includes('timeout') ? 504 : \n                       error.message.includes('not configured') ? 500 : 400;\n    \n    res.status(statusCode).json({ \n      error: error.message,\n      timestamp: new Date().toISOString()\n    });\n  }\n});\n\n// Inpainting endpoint using fal-ai/qwen-image-edit/inpaint\napp.post('/api/inpaint-image', upload.fields([{ name: 'image', maxCount: 1 }, { name: 'mask', maxCount: 1 }]), async (req, res) => {\n  try {\n    const { prompt, imageUrl, maskUrl } = req.body;\n\n    // Validate prompt\n    if (!prompt || !prompt.trim()) {\n      return res.status(400).json({ error: 'Prompt is required' });\n    }\n\n    // Get Image URL (either from file or body)\n    let finalImageUrl = imageUrl;\n    if (req.files && req.files['image'] && req.files['image'][0]) {\n      const imageFile = req.files['image'][0];\n      const imageBase64 = imageFile.buffer.toString('base64');\n      finalImageUrl = `data:${imageFile.mimetype};base64,${imageBase64}`;\n    }\n\n    // Get Mask URL (either from file or body)\n    let finalMaskUrl = maskUrl;\n    if (req.files && req.files['mask'] && req.files['mask'][0]) {\n      const maskFile = req.files['mask'][0];\n      const maskBase64 = maskFile.buffer.toString('base64');\n      finalMaskUrl = `data:${maskFile.mimetype};base64,${maskBase64}`;\n    }\n\n    if (!finalImageUrl) {\n      return res.status(400).json({ error: 'Image is required (file or imageUrl)' });\n    }\n    if (!finalMaskUrl) {\n      return res.status(400).json({ error: 'Mask is required (file or maskUrl)' });\n    }\n\n    console.log(`[INPAINT] Processing inpainting with prompt: \"${prompt.substring(0, 50)}...\"`);\n    console.log(`[INPAINT] Image source: ${finalImageUrl.substring(0, 30)}...`);\n    console.log(`[INPAINT] Mask source: ${finalMaskUrl.substring(0, 30)}...`);\n\n    // Call fal.ai API\n    if (!FAL_API_KEY) {\n      throw new Error('FAL_API_KEY not configured');\n    }\n\n    const result = await fal.subscribe('fal-ai/qwen-image-edit/inpaint', {\n      input: {\n        prompt: prompt.trim(),\n        image_url: finalImageUrl,\n        mask_url: finalMaskUrl,\n        // Explicitly use the mask as-is, avoiding any bounding box logic if the API defaults to it\n        use_mask_as_is: true\n      },\n      logs: true,\n      onQueueUpdate: (update) => {\n        if (update.status === 'IN_PROGRESS') {\n          update.logs?.map((log) => log.message).forEach((msg) => {\n            console.log(`[FAL INPAINT] ${msg}`);\n          });\n        }\n      },\n    });\n\n    console.log(`[INPAINT] Request completed, request_id: ${result.requestId}`);\n    res.json(result.data);\n\n  } catch (error) {\n    console.error('[INPAINT] Error processing inpainting:', error.message);\n    const statusCode = error.message.includes('timeout') ? 504 : \n                       error.message.includes('not configured') ? 500 : 400;\n    res.status(statusCode).json({ \n      error: error.message,\n      timestamp: new Date().toISOString()\n    });\n  }\n});\n\n// SAM2 auto-segmentation endpoint\napp.post('/api/segment-image', upload.single('image'), async (req, res) => {\n  try {\n    // Validate image\n    if (!req.file) {\n      return res.status(400).json({ error: 'No image file provided' });\n    }\n\n    // Convert image buffer to base64 data URL\n    const imageBase64 = req.file.buffer.toString('base64');\n    const imageDataUrl = `data:${req.file.mimetype};base64,${imageBase64}`;\n\n    console.log(`[SEGMENT] Processing image segmentation`);\n\n    // Call SAM2 API\n    const data = await callSam2API(imageDataUrl);\n\n    console.log('[SEGMENT] Successfully processed segmentation');\n    res.json(data);\n  } catch (error) {\n    console.error('[SEGMENT] Error processing segmentation:', error.message);\n\n    const statusCode = error.message.includes('timeout') ? 504 :\n                       error.message.includes('not configured') ? 500 : 400;\n\n    res.status(statusCode).json({\n      error: error.message,\n      timestamp: new Date().toISOString()\n    });\n  }\n});\n\n// Utility function to call SAM2 API\nasync function callSam2API(imageUrl) {\n  if (!FAL_API_KEY) {\n    throw new Error('FAL_API_KEY not configured');\n  }\n\n  try {\n    // Use fal.subscribe which handles queue polling automatically\n    const result = await fal.subscribe('fal-ai/sam2/auto-segment', {\n      input: {\n        image_url: imageUrl,\n        output_format: 'png',\n        sync_mode: true,\n      },\n      logs: true,\n      onQueueUpdate: (update) => {\n        if (update.status === 'IN_PROGRESS') {\n          update.logs?.map((log) => log.message).forEach((msg) => {\n            console.log(`[SAM2 API] ${msg}`);\n          });\n        }\n      },\n    });\n\n    console.log(`[SAM2 API] Segmentation completed, request_id: ${result.requestId}`);\n    const data = result.data;\n    \n    // Debug: Log what we received\n    console.log('[SAM2 API] Response structure:', {\n      hasCombinedMask: !!data?.combined_mask,\n      hasIndividualMasks: !!data?.individual_masks,\n      individualMasksCount: Array.isArray(data?.individual_masks) ? data.individual_masks.length : 0,\n      hasSegmentedImages: !!data?.segmented_images,\n      segmentedImagesCount: Array.isArray(data?.segmented_images) ? data.segmented_images.length : 0,\n      keys: Object.keys(data || {})\n    });\n\n    const embedMask = async (mask) => {\n      if (!mask?.url) return mask;\n      try {\n        const response = await fetch(mask.url);\n        if (!response.ok) {\n          throw new Error(`Failed to fetch mask asset: ${response.status}`);\n        }\n        const arrayBuffer = await response.arrayBuffer();\n        const base64 = Buffer.from(arrayBuffer).toString('base64');\n        const contentType = mask.content_type || 'image/png';\n        return {\n          ...mask,\n          data_url: `data:${contentType};base64,${base64}`,\n        };\n      } catch (maskError) {\n        console.warn('[SAM2 API] Unable to embed mask asset', maskError.message || maskError);\n        return mask;\n      }\n    };\n\n    if (data?.combined_mask) {\n      data.combined_mask = await embedMask(data.combined_mask);\n    }\n    if (Array.isArray(data?.individual_masks)) {\n      data.individual_masks = await Promise.all(data.individual_masks.map(embedMask));\n    }\n    if (Array.isArray(data?.segmented_images)) {\n      data.segmented_images = await Promise.all(data.segmented_images.map(embedMask));\n    }\n\n    return data;\n  } catch (error) {\n    console.error('[SAM2 API] Error:', error.message);\n    throw error;\n  }\n}\n\n// Generate image from text\napp.post('/api/generate-image', async (req, res) => {\n  try {\n    const { prompt, negativePrompt, seed, numInferenceSteps, width, height, model } = req.body;\n\n    // Validate prompt\n    if (!prompt || !prompt.trim()) {\n      return res.status(400).json({ error: 'Prompt is required' });\n    }\n\n    if (prompt.length > 2000) {\n      return res.status(400).json({ error: 'Prompt is too long (max 2000 characters)' });\n    }\n\n    // Prepare API payload\n    const payload = {\n      prompt: prompt.trim(),\n    };\n\n    // Add optional parameters\n    if (negativePrompt && negativePrompt.trim()) {\n      payload.negative_prompt = negativePrompt.trim();\n    }\n\n    const parsedSeed = parseIntParam(seed);\n    if (parsedSeed !== undefined) {\n      payload.seed = parsedSeed;\n    }\n\n    const parsedSteps = parseIntParam(numInferenceSteps);\n    if (parsedSteps !== undefined && parsedSteps > 0 && parsedSteps <= 50) {\n      payload.num_inference_steps = parsedSteps;\n    }\n\n    // Validate and add dimensions\n    const parsedWidth = parseIntParam(width, 1024);\n    const parsedHeight = parseIntParam(height, 1024);\n    \n    if (parsedWidth < 256 || parsedWidth > 2048) {\n      return res.status(400).json({ error: 'Width must be between 256 and 2048' });\n    }\n    if (parsedHeight < 256 || parsedHeight > 2048) {\n      return res.status(400).json({ error: 'Height must be between 256 and 2048' });\n    }\n\n    payload.width = parsedWidth;\n    payload.height = parsedHeight;\n\n    const modelId = model === 'pro' ? 'pro' : 'nano';\n    console.log(`[GENERATE] Generating image (${modelId}) with prompt: \"${prompt.substring(0, 50)}...\"`);\n\n    // Call fal.ai API\n    const data = await callFalAPI(payload, modelId);\n\n    console.log('[GENERATE] Successfully generated image');\n    res.json(data);\n  } catch (error) {\n    console.error('[GENERATE] Error generating image:', error.message);\n    \n    const statusCode = error.message.includes('timeout') ? 504 : \n                       error.message.includes('not configured') ? 500 : 400;\n    \n    res.status(statusCode).json({ \n      error: error.message,\n      timestamp: new Date().toISOString()\n    });\n  }\n});\n\n// Error handling middleware\napp.use((err, req, res, next) => {\n  console.error('Unhandled error:', err);\n  \n  if (err instanceof multer.MulterError) {\n    if (err.code === 'LIMIT_FILE_SIZE') {\n      return res.status(400).json({ error: 'File is too large (max 50MB)' });\n    }\n    return res.status(400).json({ error: err.message });\n  }\n  \n  res.status(500).json({ \n    error: err.message || 'Internal server error',\n    timestamp: new Date().toISOString()\n  });\n});\n\n// Serve frontend in production only (must be last)\nif (existsSync(distPath)) {\n  app.get('*', (req, res) => {\n    res.sendFile(join(__dirname, '../frontend/dist/index.html'));\n  });\n}\n\napp.listen(PORT, () => {\n  console.log(`🚀 Server running on http://localhost:${PORT}`);\n  console.log(`📝 API Key configured: ${FAL_API_KEY ? '✓' : '✗'}`);\n  if (!FAL_API_KEY) {\n    console.warn('⚠️  Warning: FAL_API_KEY not set in .env file');\n  }\n});\n"
  },
  {
    "path": "frontend/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>NanoBanana Studio - AI Image Editor</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"nanobanana-frontend\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"lucide-react\": \"^0.294.0\",\n    \"react-easy-crop\": \"^5.0.7\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.43\",\n    \"@types/react-dom\": \"^18.2.17\",\n    \"@vitejs/plugin-react\": \"^4.2.1\",\n    \"typescript\": \"^5.3.3\",\n    \"vite\": \"^5.0.8\"\n  }\n}\n\n"
  },
  {
    "path": "frontend/src/App.css",
    "content": ".app {\n  display: flex;\n  flex-direction: column;\n  height: 100vh;\n  background: #e0e7ff;\n  color: #000000;\n  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n}\n\n.top-bar {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 16px 32px;\n  background: #ffffff;\n  border-bottom: 3px solid #000000;\n  z-index: 10;\n  gap: 16px;\n  flex-wrap: wrap;\n}\n\n.brand {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n}\n\n.brand-icon {\n  color: #7c3aed;\n  font-size: 32px;\n  filter: drop-shadow(2px 2px 0px #000000);\n}\n\n.brand-title {\n  font-weight: 900;\n  font-size: 24px;\n  margin: 0;\n  color: #000000;\n  letter-spacing: -0.5px;\n  text-transform: uppercase;\n}\n\n.brand-subtitle {\n  font-size: 12px;\n  color: #000000;\n  font-weight: 700;\n  background: #a7f3d0;\n  padding: 4px 8px;\n  border: 2px solid #000000;\n  border-radius: 6px;\n  box-shadow: 2px 2px 0px #000000;\n}\n\n.menu-strip {\n  display: flex;\n  gap: 12px;\n}\n\n.menu-item {\n  background: #ffffff;\n  border: 2px solid #000000;\n  color: #000000;\n  font-size: 14px;\n  padding: 8px 16px;\n  border-radius: 8px;\n  cursor: pointer;\n  font-weight: 700;\n  transition: all 0.15s ease;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.menu-item:hover {\n  transform: translate(-1px, -1px);\n  box-shadow: 4px 4px 0px #000000;\n  background: #fef3c7;\n}\n\n.menu-item:active {\n  transform: translate(2px, 2px);\n  box-shadow: 1px 1px 0px #000000;\n}\n\n.status-cluster {\n  display: flex;\n  align-items: center;\n  gap: 16px;\n  font-size: 14px;\n  color: #000000;\n  font-weight: 700;\n}\n\n.status-dot {\n  width: 12px;\n  height: 12px;\n  border-radius: 50%;\n  display: inline-block;\n  border: 2px solid #000000;\n}\n\n.status-dot.online {\n  background: #10b981;\n}\n\n.status-dot.offline {\n  background: #ef4444;\n}\n\n.quick-action-nav {\n  display: flex;\n  gap: 8px;\n  flex: 1;\n  justify-content: center;\n  flex-wrap: wrap;\n  min-width: 220px;\n}\n\n.quick-action-icon-btn {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  padding: 8px 12px;\n  border-radius: 999px;\n  cursor: pointer;\n  font-size: 12px;\n  font-weight: 800;\n  text-transform: uppercase;\n  transition: all 0.15s ease;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.quick-action-icon-btn:disabled {\n  opacity: 0.4;\n  cursor: not-allowed;\n  box-shadow: none;\n}\n\n.quick-action-icon-btn:not(:disabled):hover {\n  background: #c4b5fd;\n  transform: translate(-1px, -1px);\n  box-shadow: 4px 4px 0px #000000;\n}\n\n.secondary-btn {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  padding: 8px 16px;\n  border-radius: 8px;\n  cursor: pointer;\n  font-size: 13px;\n  font-weight: 700;\n  transition: all 0.15s ease;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.secondary-btn:hover {\n  transform: translate(-1px, -1px);\n  box-shadow: 4px 4px 0px #000000;\n  background: #e9d5ff;\n}\n\n.workspace {\n  flex: 1;\n  display: grid;\n  grid-template-columns: 80px 1fr 380px;\n  overflow: hidden;\n  background: #e0e7ff;\n  gap: 0;\n}\n\n.tool-panel {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  padding: 24px 12px;\n  background: #ffffff;\n  border-right: 3px solid #000000;\n  z-index: 5;\n}\n\n.tool-btn {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 8px;\n  padding: 16px 8px;\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  font-size: 11px;\n  font-weight: 800;\n  border-radius: 8px;\n  cursor: pointer;\n  transition: all 0.15s ease;\n  text-transform: uppercase;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.tool-btn:disabled {\n  opacity: 0.5;\n  cursor: not-allowed;\n  box-shadow: none;\n  background: #e5e7eb;\n}\n\n.tool-btn svg {\n  color: inherit;\n  font-size: 24px;\n}\n\n.tool-btn.active {\n  background: #fbbf24;\n  color: #000000;\n  transform: translate(-1px, -1px);\n  box-shadow: 4px 4px 0px #000000;\n}\n\n.tool-btn:not(:disabled):not(.active):hover {\n  background: #e0f2fe;\n  transform: translate(-1px, -1px);\n  box-shadow: 4px 4px 0px #000000;\n}\n\n.canvas-shell {\n  display: flex;\n  flex-direction: column;\n  background: #ffffff;\n  overflow: hidden;\n  border-right: 3px solid #000000;\n}\n\n.options-bar {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 16px 32px;\n  background: #ffedd5;\n  border-bottom: 3px solid #000000;\n}\n\n.document-title {\n  margin: 0;\n  font-size: 20px;\n  font-weight: 900;\n  color: #000000;\n  letter-spacing: -0.5px;\n  text-transform: uppercase;\n}\n\n.document-subtitle {\n  font-size: 13px;\n  color: #000000;\n  font-weight: 700;\n  opacity: 0.7;\n}\n\n.mode-toggle.chips {\n  display: flex;\n  gap: 8px;\n  background: #ffffff;\n  padding: 6px;\n  border-radius: 8px;\n  border: 2px solid #000000;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.mode-chip {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  padding: 8px 16px;\n  border-radius: 6px;\n  border: 2px solid transparent;\n  background: transparent;\n  color: #000000;\n  cursor: pointer;\n  font-size: 14px;\n  font-weight: 700;\n  transition: all 0.15s ease;\n}\n\n.mode-chip.active {\n  background: #000000;\n  color: #ffffff;\n  box-shadow: 2px 2px 0px #a78bfa;\n}\n\n.mode-chip:not(.active):hover {\n  background: #e5e7eb;\n}\n\n.model-toggle {\n  display: flex;\n  gap: 6px;\n  background: #ffffff;\n  padding: 6px;\n  border-radius: 8px;\n  border: 2px solid #000000;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.model-chip {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  padding: 6px 12px;\n  border-radius: 6px;\n  border: 2px solid transparent;\n  background: transparent;\n  color: #000000;\n  cursor: pointer;\n  font-size: 12px;\n  font-weight: 700;\n  transition: all 0.15s ease;\n  min-width: 140px;\n}\n\n.model-chip span {\n  font-size: 10px;\n  font-weight: 600;\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  opacity: 0.7;\n}\n\n.model-chip.active {\n  background: #000000;\n  color: #ffffff;\n  box-shadow: 2px 2px 0px #a78bfa;\n}\n\n.model-chip:not(.active):hover {\n  background: #e5e7eb;\n  transform: translate(-1px, -1px);\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.canvas-stage {\n  flex: 1;\n  display: grid;\n  grid-template-columns: minmax(0, 1fr) 320px;\n  overflow: hidden;\n  gap: 0;\n  background: #e0e7ff;\n}\n\n.canvas-area {\n  background-color: #ffffff;\n  background-image: radial-gradient(#e5e7eb 1px, transparent 1px);\n  background-size: 20px 20px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 40px;\n  position: relative;\n}\n\n.canvas-area.pro {\n  border-right: 3px solid #000000;\n}\n\n.empty-state {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 20px;\n  color: #000000;\n  text-align: center;\n}\n\n.empty-state svg {\n  font-size: 80px;\n  color: #a78bfa;\n  opacity: 1;\n  filter: drop-shadow(3px 3px 0px #000000);\n}\n\n.empty-state p {\n  font-size: 24px;\n  font-weight: 900;\n  margin: 0;\n  color: #000000;\n  letter-spacing: -0.5px;\n  text-transform: uppercase;\n}\n\n.hint {\n  font-size: 14px;\n  color: #000000;\n  font-weight: 700;\n  background: #ddd6fe;\n  padding: 4px 12px;\n  border: 2px solid #000000;\n  border-radius: 6px;\n}\n\n.image-container {\n  position: relative;\n  max-width: 100%;\n  max-height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.image-container.framed {\n  padding: 20px;\n  border-radius: 12px;\n  background: #ffffff;\n  box-shadow: 6px 6px 0px #000000;\n  border: 3px solid #000000;\n}\n\n.result-image {\n  max-width: 100%;\n  max-height: calc(100vh - 200px);\n  border-radius: 4px;\n  border: 2px solid #000000;\n}\n\n.image-container.cropping {\n  overflow: hidden;\n}\n\n.cropper-wrapper {\n  position: relative;\n  width: min(90vw, 900px);\n  height: min(65vh, 520px);\n  border-radius: 4px;\n  overflow: hidden;\n  border: 3px solid #000000;\n  box-shadow: 6px 6px 0px #000000;\n}\n\n.image-actions.floating {\n  position: absolute;\n  right: 32px;\n  bottom: 32px;\n}\n\n.download-btn {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  padding: 12px 24px;\n  border-radius: 8px;\n  border: 2px solid #000000;\n  background: #10b981;\n  color: #000000;\n  cursor: pointer;\n  font-size: 14px;\n  font-weight: 800;\n  letter-spacing: 0.5px;\n  text-transform: uppercase;\n  box-shadow: 4px 4px 0px #000000;\n  transition: all 0.15s ease;\n}\n\n.download-btn:hover {\n  transform: translate(-2px, -2px);\n  box-shadow: 6px 6px 0px #000000;\n  background: #34d399;\n}\n\n.stack-panels {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  padding: 20px;\n  overflow-y: auto;\n  background: #ffffff;\n  border-left: 3px solid #000000;\n}\n\n/* Custom scrollbar */\n.stack-panels::-webkit-scrollbar,\n.control-panel::-webkit-scrollbar {\n  width: 10px;\n}\n\n.stack-panels::-webkit-scrollbar-track,\n.control-panel::-webkit-scrollbar-track {\n  background: #f3f4f6;\n  border-left: 2px solid #000000;\n}\n\n.stack-panels::-webkit-scrollbar-thumb,\n.control-panel::-webkit-scrollbar-thumb {\n  background: #000000;\n  border: 2px solid #000000;\n}\n\n.stack-panels::-webkit-scrollbar-thumb:hover,\n.control-panel::-webkit-scrollbar-thumb:hover {\n  background: #7c3aed;\n}\n\n.panel-card {\n  background: #ffffff;\n  border-radius: 12px;\n  padding: 20px;\n  border: 2px solid #000000;\n  box-shadow: 4px 4px 0px #000000;\n}\n\n.panel-card.subtle {\n  background: #fef3c7;\n  border-color: #000000;\n}\n\n.panel-header-info {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  color: #000000;\n  font-weight: 800;\n  text-transform: uppercase;\n}\n\n.layer-add-btn {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  padding: 8px 12px;\n  border-radius: 6px;\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  font-size: 12px;\n  font-weight: 800;\n  text-transform: uppercase;\n  cursor: pointer;\n  transition: all 0.15s ease;\n  box-shadow: 2px 2px 0px #000000;\n}\n\n.layer-add-btn:hover {\n  background: #c4b5fd;\n  transform: translate(-1px, -1px);\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.panel-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 10px;\n  font-size: 14px;\n  color: #000000;\n  margin-bottom: 16px;\n}\n\n.panel-header h2 {\n  font-size: 18px;\n  font-weight: 900;\n  margin: 0;\n  color: #000000;\n  letter-spacing: -0.5px;\n  text-transform: uppercase;\n}\n\n.layer-list {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.layer-item {\n  width: 100%;\n  text-align: left;\n  padding: 12px 16px;\n  border-radius: 8px;\n  background: #ffffff;\n  font-size: 14px;\n  border: 2px solid #000000;\n  color: #000000;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  transition: all 0.15s ease;\n  box-shadow: 2px 2px 0px #000000;\n}\n\n.layer-item:hover {\n  background: #fef3c7;\n  transform: translate(-1px, -1px);\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.layer-item.active {\n  background: #c4b5fd;\n  color: #000000;\n  border-color: #000000;\n  box-shadow: 3px 3px 0px #000000;\n  transform: translate(-1px, -1px);\n}\n\n.layer-item.muted {\n  color: #6b7280;\n  background: #e5e7eb;\n}\n\n.layer-item.hidden {\n  opacity: 0.6;\n}\n\n.layer-meta {\n  display: flex;\n  flex-direction: column;\n  gap: 2px;\n}\n\n.layer-meta strong {\n  font-size: 14px;\n  font-weight: 800;\n  color: #000000;\n}\n\n.layer-meta span {\n  font-size: 11px;\n  color: #000000;\n  font-weight: 600;\n  text-transform: uppercase;\n}\n\n.layer-pill {\n  padding: 4px 8px;\n  border-radius: 4px;\n  font-size: 10px;\n  text-transform: uppercase;\n  font-weight: 800;\n  letter-spacing: 0.5px;\n  border: 2px solid #000000;\n  box-shadow: 1px 1px 0px #000000;\n}\n\n.layer-pill.source {\n  background: #ffffff;\n  color: #000000;\n}\n\n.layer-pill.ai {\n  background: #a78bfa;\n  color: #000000;\n}\n\n.layer-pill.segment {\n  background: #fca5a5;\n  color: #000000;\n}\n\n.layer-pill.empty {\n  background: #fcd34d;\n  color: #000000;\n}\n\n.layer-actions {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.layer-eye-btn {\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  padding: 6px;\n  border-radius: 6px;\n  cursor: pointer;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  transition: all 0.15s ease;\n}\n\n.layer-eye-btn:hover {\n  background: #a7f3d0;\n  transform: translate(-1px, -1px);\n  box-shadow: 2px 2px 0px #000000;\n}\n\n.history-list {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n  font-size: 13px;\n  color: #000000;\n}\n\n.history-list li {\n  padding: 8px 12px;\n  background: #ffffff;\n  border-radius: 6px;\n  font-weight: 600;\n  border: 2px solid #000000;\n  box-shadow: 2px 2px 0px #000000;\n}\n\n.control-panel {\n  padding: 20px;\n  overflow-y: auto;\n  background: #ffffff;\n  border-left: 3px solid #000000;\n}\n\n.upload-deck {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  margin-bottom: 20px;\n}\n\n.file-input {\n  display: none;\n}\n\n.upload-btn,\n.clear-btn {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 10px;\n  padding: 16px 20px;\n  border-radius: 8px;\n  border: 2px dashed #000000;\n  background: #f3f4f6;\n  color: #000000;\n  cursor: pointer;\n  font-weight: 800;\n  font-size: 14px;\n  text-transform: uppercase;\n  transition: all 0.15s ease;\n}\n\n.upload-btn:hover,\n.clear-btn:hover {\n  background: #e0e7ff;\n  border-color: #7c3aed;\n  border-style: solid;\n  transform: translate(-2px, -2px);\n  box-shadow: 4px 4px 0px #000000;\n}\n\n.clear-btn.ghost {\n  background: #fee2e2;\n  color: #000000;\n  border: 2px solid #000000;\n}\n\n.clear-btn.ghost:hover {\n  background: #ef4444;\n  color: #ffffff;\n}\n\n.prompt-section {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  margin-bottom: 20px;\n}\n\n.prompt-header {\n  display: flex;\n  justify-content: space-between;\n  font-size: 13px;\n  color: #000000;\n  font-weight: 800;\n  text-transform: uppercase;\n}\n\n.char-count {\n  color: #7c3aed;\n  font-weight: 800;\n}\n\n.prompt-input {\n  width: 100%;\n  padding: 16px;\n  border-radius: 8px;\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  resize: none;\n  font-size: 16px;\n  font-family: inherit;\n  font-weight: 600;\n  transition: all 0.15s ease;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.prompt-input::placeholder {\n  color: #9ca3af;\n}\n\n.prompt-input:focus {\n  outline: none;\n  border-color: #7c3aed;\n  background: #ffffff;\n  transform: translate(-1px, -1px);\n  box-shadow: 5px 5px 0px #000000;\n}\n\n.crop-controls {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  padding: 20px;\n  border-radius: 12px;\n  border: 2px solid #000000;\n  background: #f3f4f6;\n  margin-bottom: 20px;\n  box-shadow: 4px 4px 0px #000000;\n}\n\n.crop-header p {\n  margin: 0;\n  font-weight: 800;\n  color: #000000;\n  text-transform: uppercase;\n}\n\n.crop-header span {\n  font-size: 12px;\n  color: #000000;\n  font-weight: 600;\n}\n\n.aspect-options {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n}\n\n.aspect-chip {\n  padding: 8px 16px;\n  border-radius: 6px;\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  font-size: 13px;\n  font-weight: 700;\n  cursor: pointer;\n  transition: all 0.15s ease;\n  box-shadow: 2px 2px 0px #000000;\n}\n\n.aspect-chip.active {\n  background: #000000;\n  color: #ffffff;\n  transform: translate(-1px, -1px);\n  box-shadow: 3px 3px 0px #a78bfa;\n}\n\n.aspect-chip:not(.active):hover {\n  background: #e5e7eb;\n  transform: translate(-1px, -1px);\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.crop-label {\n  font-size: 13px;\n  color: #000000;\n  font-weight: 800;\n  text-transform: uppercase;\n}\n\n.crop-slider {\n  width: 100%;\n  height: 12px;\n  border-radius: 6px;\n  background: #ffffff;\n  border: 2px solid #000000;\n  outline: none;\n  -webkit-appearance: none;\n}\n\n.crop-slider::-webkit-slider-thumb {\n  -webkit-appearance: none;\n  appearance: none;\n  width: 20px;\n  height: 20px;\n  border-radius: 4px;\n  background: #7c3aed;\n  border: 2px solid #000000;\n  cursor: pointer;\n  box-shadow: 2px 2px 0px #000000;\n}\n\n.crop-slider::-moz-range-thumb {\n  width: 20px;\n  height: 20px;\n  border-radius: 4px;\n  background: #7c3aed;\n  border: 2px solid #000000;\n  cursor: pointer;\n  box-shadow: 2px 2px 0px #000000;\n}\n\n.crop-actions {\n  display: flex;\n  justify-content: flex-end;\n  gap: 12px;\n}\n\n.primary-btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  padding: 12px 24px;\n  border-radius: 8px;\n  border: 2px solid #000000;\n  background: #7c3aed;\n  color: #ffffff;\n  font-weight: 800;\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  cursor: pointer;\n  box-shadow: 4px 4px 0px #000000;\n  transition: all 0.15s ease;\n}\n\n.primary-btn:disabled {\n  opacity: 0.5;\n  cursor: not-allowed;\n  background: #9ca3af;\n}\n\n.primary-btn:not(:disabled):hover {\n  transform: translate(-2px, -2px);\n  box-shadow: 6px 6px 0px #000000;\n  background: #8b5cf6;\n}\n\n.filters-panel {\n  background: #ffffff;\n  border-radius: 12px;\n  padding: 20px;\n  border: 2px solid #000000;\n  box-shadow: 4px 4px 0px #000000;\n  margin-bottom: 20px;\n}\n\n.filters-grid {\n  display: grid;\n  grid-template-columns: repeat(2, minmax(0, 1fr));\n  gap: 12px;\n  margin-bottom: 16px;\n}\n\n@media (max-width: 520px) {\n  .filters-grid {\n    grid-template-columns: 1fr;\n  }\n}\n\n.filter-chip {\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  padding: 16px 10px;\n  border-radius: 8px;\n  cursor: pointer;\n  text-align: center;\n  transition: all 0.15s ease;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.filter-chip:hover {\n  background: #ddd6fe;\n  transform: translate(-2px, -2px);\n  box-shadow: 5px 5px 0px #000000;\n}\n\n.filter-label {\n  font-size: 13px;\n  font-weight: 800;\n  text-transform: uppercase;\n  margin-bottom: 4px;\n  display: block;\n}\n\n.filter-category {\n  font-size: 10px;\n  font-weight: 700;\n  letter-spacing: 0.5px;\n  color: #6b7280;\n  text-transform: uppercase;\n  background: #e5e7eb;\n  padding: 2px 6px;\n  border-radius: 4px;\n  display: inline-block;\n  border: 1px solid #000000;\n}\n\n.filter-instructions {\n  background: #fef3c7;\n  border: 2px solid #000000;\n  padding: 14px;\n  border-radius: 8px;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.filter-instructions p {\n  margin: 0;\n  font-size: 13px;\n  font-weight: 700;\n  color: #000000;\n}\n\n.preset-grid {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 12px;\n  margin-bottom: 24px;\n}\n\n.preset-chip {\n  padding: 10px 16px;\n  border-radius: 6px;\n  border: 2px solid #000000;\n  background: #ffffff;\n  color: #000000;\n  font-size: 13px;\n  font-weight: 700;\n  text-transform: uppercase;\n  cursor: pointer;\n  box-shadow: 3px 3px 0px #000000;\n  transition: all 0.15s ease;\n}\n\n.preset-chip:hover {\n  background: #fef3c7;\n  transform: translate(-1px, -1px);\n  box-shadow: 4px 4px 0px #000000;\n}\n\n.edit-btn {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 12px;\n  padding: 16px 32px;\n  width: 100%;\n  border-radius: 8px;\n  border: 2px solid #000000;\n  background: #7c3aed;\n  color: #ffffff;\n  font-weight: 900;\n  font-size: 16px;\n  text-transform: uppercase;\n  letter-spacing: 1px;\n  cursor: pointer;\n  box-shadow: 4px 4px 0px #000000;\n  transition: all 0.15s ease;\n}\n\n.edit-btn:hover:not(:disabled) {\n  background: #8b5cf6;\n  transform: translate(-2px, -2px);\n  box-shadow: 6px 6px 0px #000000;\n}\n\n.edit-btn:disabled {\n  opacity: 0.5;\n  cursor: not-allowed;\n  background: #9ca3af;\n}\n\n.warning-message,\n.error-message {\n  margin-top: 16px;\n  padding: 14px 18px;\n  border-radius: 8px;\n  font-size: 14px;\n  font-weight: 700;\n  border: 2px solid #000000;\n  box-shadow: 3px 3px 0px #000000;\n}\n\n.warning-message {\n  background: #fef3c7;\n  color: #000000;\n}\n\n.error-message {\n  background: #fee2e2;\n  color: #000000;\n}\n\n.status-bar {\n  height: 40px;\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  gap: 24px;\n  padding: 0 32px;\n  font-size: 13px;\n  background: #ffffff;\n  border-top: 3px solid #000000;\n  color: #000000;\n  font-weight: 700;\n  text-transform: uppercase;\n}\n\n.spinner {\n  animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n/* Canvas and segmentation styling */\n.canvas-stack {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  /* Checkerboard pattern to show transparency */\n  background-image:\n    linear-gradient(45deg, #f8fafc 25%, #e2e8f0 25%),\n    linear-gradient(-45deg, #f8fafc 25%, #e2e8f0 25%),\n    linear-gradient(45deg, #e2e8f0 75%, #f8fafc 75%),\n    linear-gradient(-45deg, #e2e8f0 75%, #f8fafc 75%);\n  background-size: 20px 20px;\n  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;\n  border-radius: 12px;\n  border: 1px solid rgba(148, 163, 184, 0.2);\n}\n\n.segmentation-overlay {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: auto;\n}\n\n.segment-mask {\n  transition: all 0.2s ease;\n}\n\n.segment-mask.selected {\n  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.5);\n}\n\n.segment-mask.hidden {\n  opacity: 0;\n}\n\n.segmentation-loading {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 16px;\n  background: rgba(255, 255, 255, 0.95);\n  backdrop-filter: blur(20px);\n  padding: 24px 32px;\n  border-radius: 16px;\n  color: #1e293b;\n  font-size: 14px;\n  font-weight: 600;\n  letter-spacing: -0.2px;\n  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n  border: 1px solid rgba(148, 163, 184, 0.2);\n}\n\n.selection-box {\n  position: absolute;\n  border: 2px dashed #6366f1;\n  border-radius: 8px;\n  pointer-events: none;\n  box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);\n}\n\n@keyframes dash {\n  to {\n    stroke-dashoffset: -24;\n  }\n}\n"
  },
  {
    "path": "frontend/src/App.tsx",
    "content": "import { useState, useRef, useEffect, useCallback, useMemo } from 'react';\nimport {\n  ImageIcon,\n  Upload,\n  Download,\n  Sparkles,\n  Loader2,\n  X,\n  Wand2,\n  Layers,\n  History,\n  Settings2,\n  MousePointer2,\n  Crop,\n  Plus,\n  Eye,\n  EyeOff,\n  Eraser,\n  SunMedium,\n  Camera,\n  Zap,\n  Sparkles as SparklesIcon,\n  Palette,\n} from 'lucide-react';\nimport type { LucideIcon } from 'lucide-react';\nimport Cropper, { Area } from 'react-easy-crop';\nimport { Mode, ModelId } from './types';\nimport { editImage, generateImage, segmentImage, checkHealth } from './api';\nimport { validateImageFile, extractImageUrl, dataUrlToBlob, downloadImage, getTimestamp } from './utils';\nimport './App.css';\nimport 'react-easy-crop/react-easy-crop.css';\n\ntype LayerKind = 'source' | 'ai' | 'empty' | 'segment';\n\ninterface BoundingBox {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\ninterface Layer {\n  id: string;\n  name: string;\n  kind: LayerKind;\n  preview?: string | null;\n  timestamp: string;\n  visible?: boolean;\n  metadata?: {\n    maskUrl?: string;\n    boundingBox?: BoundingBox | null;\n  };\n}\n\ninterface QuickEditAction {\n  label: string;\n  description: string;\n  prompt: string;\n  negativePrompt?: string;\n  icon: LucideIcon;\n}\n\nfunction App() {\n  const [image, setImage] = useState<string | null>(null);\n  const [editedImage, setEditedImage] = useState<string | null>(null);\n  const [prompt, setPrompt] = useState('');\n  const [negativePrompt, setNegativePrompt] = useState('');\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [mode, setMode] = useState<Mode>('edit');\n  const [apiConfigured, setApiConfigured] = useState(true);\n  const [activeTool, setActiveTool] = useState('select');\n  const [layers, setLayers] = useState<Layer[]>([]);\n  const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);\n  const [showCropper, setShowCropper] = useState(false);\n  const [cropTargetLayerId, setCropTargetLayerId] = useState<string | null>(null);\n  const [crop, setCrop] = useState({ x: 0, y: 0 });\n  const [zoom, setZoom] = useState(1);\n  const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);\n  const [aiLayerCount, setAiLayerCount] = useState(0);\n  const [adjustmentCount, setAdjustmentCount] = useState(1);\n  const [aspect, setAspect] = useState<number | undefined>(3 / 2);\n  const [isSegmenting, setIsSegmenting] = useState(false);\n  const [model, setModel] = useState<ModelId>('nano');\n  const fileInputRef = useRef<HTMLInputElement>(null);\n\n  const menuItems = ['Export', 'Help'];\n  const toolset = [\n    { id: 'select', label: 'Select', icon: MousePointer2 },\n    { id: 'crop', label: 'Crop', icon: Crop },\n    { id: 'filters', label: 'Filters', icon: Sparkles },\n    { id: 'magic', label: 'Magic', icon: Wand2 },\n  ];\n  const presetPrompts = [\n    {\n      label: 'Portrait glow',\n      prompt:\n        'Hyper-real studio portrait, cinematic rim lighting, polished skin texture, subtle bokeh background, warm highlights',\n    },\n    {\n      label: 'Cinematic matte',\n      prompt:\n        'Moody widescreen grade, teal and amber palette, lifted blacks, film grain, dramatic contrast, cinematic atmosphere',\n    },\n    {\n      label: 'Product polish',\n      prompt:\n        'Luxury product beauty lighting, reflective podium, glossy highlights, high-contrast shadows, editorial look',\n    },\n    {\n      label: 'Golden hour',\n      prompt:\n        'Sunset hues, soft volumetric light, warm peach glow, long natural shadows, coastal warmth, dreamy tone',\n    },\n    {\n      label: 'Analog film',\n      prompt:\n        'Kodak portra palette, soft halation, subtle film grain, gentle fades, authentic analog imperfections',\n    },\n  ];\n\n  const filterPresets = [\n    {\n      label: 'Vintage Film',\n      prompt: 'Kodak Portra 400 film, warm amber tones, subtle grain texture, vintage color grading, authentic film imperfections, cinematic look',\n      category: 'Film'\n    },\n    {\n      label: 'Black & White',\n      prompt: 'High contrast black and white, dramatic shadows, rich textures, classic photography, silver gelatin print aesthetic, moody atmosphere',\n      category: 'Monochrome'\n    },\n    {\n      label: 'Sepia Tone',\n      prompt: 'Warm sepia tones, vintage photograph, antique brown coloring, historical aesthetic, aged paper texture, nostalgic atmosphere',\n      category: 'Vintage'\n    },\n    {\n      label: 'High Contrast',\n      prompt: 'Extreme contrast, deep blacks, bright highlights, dramatic lighting, bold shadows, punchy colors, vibrant saturation',\n      category: 'Dramatic'\n    },\n    {\n      label: 'Soft Glow',\n      prompt: 'Soft dreamy glow, ethereal lighting, gentle highlights, warm ambiance, romantic atmosphere, subtle soft focus, luminous quality',\n      category: 'Dreamy'\n    },\n    {\n      label: 'Cinematic Teal',\n      prompt: 'Cinematic color grading, teal and orange palette, film look, lifted blacks, subtle grain, Hollywood cinematography style',\n      category: 'Cinematic'\n    },\n    {\n      label: 'Neon Glow',\n      prompt: 'Vibrant neon colors, cyberpunk aesthetic, electric blues and pinks, glowing highlights, futuristic atmosphere, high saturation',\n      category: 'Modern'\n    },\n    {\n      label: 'Matte Flat',\n      prompt: 'Flat design aesthetic, minimal shadows, clean lighting, modern photography, reduced contrast, contemporary style, flat lay',\n      category: 'Minimal'\n    },\n    {\n      label: 'Golden Hour',\n      prompt: 'Golden hour lighting, warm sunset tones, long dramatic shadows, magical atmosphere, romantic golden glow, evening light',\n      category: 'Lighting'\n    },\n    {\n      label: 'Vintage Polaroid',\n      prompt: 'Polaroid instant film, faded colors, white border, authentic polaroid aesthetic, nostalgic snapshot, retro photography',\n      category: 'Instant'\n    },\n    {\n      label: 'Studio Lighting',\n      prompt: 'Professional studio lighting, clean white background, even illumination, commercial photography, product photography style',\n      category: 'Studio'\n    },\n    {\n      label: 'Moody Noir',\n      prompt: 'Film noir style, high contrast, deep shadows, dramatic lighting, black and white with blue tint, mysterious atmosphere',\n      category: 'Noir'\n    }\n  ];\n  const quickEdits: QuickEditAction[] = [\n    {\n      label: 'Clean Background',\n      description: 'Isolate your subject on a soft studio gradient.',\n      prompt:\n        'Isolate the main subject, remove distractions, replace background with a soft neutral gradient backdrop, keep natural shadows, commercial studio polish',\n      negativePrompt: 'busy background, clutter, extra hands, text, watermark',\n      icon: Eraser,\n    },\n    {\n      label: 'Product Pop',\n      description: 'Boost contrast, reflections, and clarity.',\n      prompt:\n        'Create a premium e-commerce hero shot, punchy contrast, sharpened edges, controlled reflections, glossy highlights, gradient sweep backdrop',\n      negativePrompt: 'noise, watermark, text overlay, harsh artifacts',\n      icon: Camera,\n    },\n    {\n      label: 'Portrait Glow',\n      description: 'Retouch skin, add warm rim lighting.',\n      prompt:\n        'Subtle portrait retouch, even skin tone, soften blemishes, add warm golden rim light, cinematic bokeh background, high-end magazine aesthetic',\n      negativePrompt: 'over-smoothing, plastic skin, distortion, vignette',\n      icon: SunMedium,\n    },\n    {\n      label: 'Cinematic Mood',\n      description: 'Teal & amber film-grade look.',\n      prompt:\n        'Apply dramatic teal and amber cinematic grade, lifted blacks, gentle bloom, volumetric atmosphere, film grain, widescreen energy',\n      negativePrompt: 'washed out, oversaturated, text overlay',\n      icon: Palette,\n    },\n    {\n      label: 'Vibrant Neon',\n      description: 'Add cyberpunk neon accents.',\n      prompt:\n        'Introduce neon magenta and cyan rim lighting, subtle glow trails, futuristic highlights, reflective surfaces, cyberpunk energy',\n      negativePrompt: 'overexposed, posterization, text badge',\n      icon: Zap,\n    },\n    {\n      label: 'Matte Vintage',\n      description: 'Soft matte finish with retro tones.',\n      prompt:\n        'Apply vintage matte film look, muted shadows, gentle halation, warm highlights, dusted texture, analog imperfections',\n      negativePrompt: 'heavy grain, scratches, frame border, text',\n      icon: SparklesIcon,\n    },\n  ];\n  const modelOptions: Array<{ id: ModelId; label: string; description: string }> = [\n    {\n      id: 'nano',\n      label: 'Nano Banana',\n      description: 'Fast & lightweight',\n    },\n    {\n      id: 'pro',\n      label: 'Nano Banana Pro',\n      description: 'SOTA fidelity',\n    },\n  ];\n  const aspectPresets = [\n    { label: 'Free', ratio: undefined },\n    { label: '1:1', ratio: 1 },\n    { label: '4:5', ratio: 4 / 5 },\n    { label: '3:2', ratio: 3 / 2 },\n    { label: '16:9', ratio: 16 / 9 },\n  ];\n\nconst registerLayer = useCallback(\n  (\n    layerData: Omit<Layer, 'id' | 'timestamp'>,\n    options?: { replaceKind?: LayerKind; autoSelect?: boolean },\n  ) => {\n      const layer: Layer = {\n        id: `layer-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,\n        timestamp: new Date().toLocaleTimeString(),\n        ...layerData,\n      visible: layerData.visible ?? true,\n      };\n\n    setLayers((prev) => {\n        let base = prev;\n        if (options?.replaceKind) {\n          base = prev.filter((entry) => entry.kind !== options.replaceKind);\n        }\n        return [layer, ...base];\n      });\n\n    if (options?.autoSelect !== false) {\n      setSelectedLayerId(layer.id);\n    }\n      return layer;\n    },\n    [],\n  );\n\n  const loadImageElement = useCallback((src: string) => {\n    return new Promise<HTMLImageElement>((resolve, reject) => {\n      const img = new Image();\n      img.crossOrigin = 'anonymous';\n      img.onload = () => {\n        console.log(`[IMAGE LOAD] Loaded image, src start: ${src.substring(0, 60)}..., size: ${img.width}x${img.height}`);\n        resolve(img);\n      };\n      img.onerror = (e) => {\n        console.error(`[IMAGE LOAD] Failed to load image, src start: ${src.substring(0, 60)}...`, e);\n        reject(e);\n      };\n      img.src = src;\n    });\n  }, []);\n\n  const createMaskedPreview = useCallback((baseImage: HTMLImageElement, maskImage: HTMLImageElement) => {\n    const width = baseImage.width;\n    const height = baseImage.height;\n    const canvas = document.createElement('canvas');\n    const ctx = canvas.getContext('2d');\n\n    if (!ctx) {\n      throw new Error('Unable to create canvas context for segmentation preview');\n    }\n\n    canvas.width = width;\n    canvas.height = height;\n    \n    // Clear canvas to transparent\n    ctx.clearRect(0, 0, width, height);\n    \n    // First, check what the mask looks like\n    const maskCanvas = document.createElement('canvas');\n    const maskCtx = maskCanvas.getContext('2d');\n    if (maskCtx) {\n      maskCanvas.width = maskImage.width;\n      maskCanvas.height = maskImage.height;\n      maskCtx.drawImage(maskImage, 0, 0);\n      const maskData = maskCtx.getImageData(0, 0, Math.min(100, maskImage.width), Math.min(100, maskImage.height));\n      let whitePixels = 0;\n      let blackPixels = 0;\n      let transparentPixels = 0;\n      let opaquePixels = 0;\n      for (let i = 0; i < maskData.data.length; i += 4) {\n        const r = maskData.data[i];\n        const g = maskData.data[i + 1];\n        const b = maskData.data[i + 2];\n        const a = maskData.data[i + 3];\n        if (a > 250) opaquePixels++;\n        if (a < 10) transparentPixels++;\n        else if (r > 200 && g > 200 && b > 200) whitePixels++;\n        else if (r < 55 && g < 55 && b < 55) blackPixels++;\n      }\n      // Debug mask content (uncomment if needed)\n      // console.log('[MASK] Mask analysis:', { \n      //   whitePixels, \n      //   blackPixels, \n      //   transparentPixels, \n      //   opaquePixels,\n      //   totalSampled: maskData.data.length / 4,\n      //   percentWhite: (whitePixels / (maskData.data.length / 4) * 100).toFixed(1) + '%',\n      //   percentOpaque: (opaquePixels / (maskData.data.length / 4) * 100).toFixed(1) + '%'\n      // });\n    }\n    \n    // Draw the base image\n    ctx.drawImage(baseImage, 0, 0, width, height);\n    \n    // Create a temporary canvas for the mask to analyze it\n    const tempCanvas = document.createElement('canvas');\n    const tempCtx = tempCanvas.getContext('2d');\n    if (!tempCtx) {\n      throw new Error('Unable to create temporary canvas context');\n    }\n    tempCanvas.width = width;\n    tempCanvas.height = height;\n    tempCtx.drawImage(maskImage, 0, 0, width, height);\n    const maskData = tempCtx.getImageData(0, 0, width, height);\n    \n    // Get the base image data\n    const imageData = ctx.getImageData(0, 0, width, height);\n    \n    // Apply mask: Check both alpha channel and RGB values\n    // SAM2 masks can be: 1) Alpha channel mask, 2) RGB grayscale, or 3) Inverted\n    let hasAlphaVariation = false;\n    let hasRGBVariation = false;\n    \n    // Sample to detect mask type\n    for (let i = 0; i < Math.min(1000, maskData.data.length); i += 4) {\n      if (maskData.data[i + 3] < 255) hasAlphaVariation = true;\n      if (maskData.data[i] !== maskData.data[i + 3]) hasRGBVariation = true;\n    }\n    \n    console.log('[MASK] Detected mask type:', { hasAlphaVariation, hasRGBVariation });\n    \n    // Apply the mask and count non-transparent pixels\n    let opaquePixelCount = 0;\n    let semiTransparentCount = 0;\n    let transparentCount = 0;\n    \n    for (let i = 0; i < imageData.data.length; i += 4) {\n      if (hasAlphaVariation) {\n        // Use alpha channel directly\n        imageData.data[i + 3] = maskData.data[i + 3];\n      } else {\n        // Use RGB value as alpha (standard: white = keep, black = remove)\n        const maskValue = maskData.data[i]; // Red channel\n        imageData.data[i + 3] = maskValue; // White (255) = opaque, Black (0) = transparent\n      }\n      \n      const alpha = imageData.data[i + 3];\n      if (alpha > 200) opaquePixelCount++;\n      else if (alpha > 50) semiTransparentCount++;\n      else transparentCount++;\n    }\n    \n    const totalPixels = imageData.data.length / 4;\n    console.log('[MASK] Final pixel counts:', {\n      opaque: opaquePixelCount,\n      semiTransparent: semiTransparentCount,\n      transparent: transparentCount,\n      total: totalPixels,\n      percentOpaque: ((opaquePixelCount / totalPixels) * 100).toFixed(1) + '%'\n    });\n    \n    ctx.putImageData(imageData, 0, 0);\n\n    const result = canvas.toDataURL('image/png');\n    \n    // Debug: Check if the result is different from the original (uncomment if needed)\n    // console.log('[MASK] Created preview', {\n    //   baseSize: `${baseImage.width}x${baseImage.height}`,\n    //   maskSize: `${maskImage.width}x${maskImage.height}`,\n    //   canvasSize: `${canvas.width}x${canvas.height}`,\n    //   resultLength: result.length\n    // });\n\n    return result;\n  }, []);\n\n  const extractBoundingBox = useCallback((maskImage: HTMLImageElement): BoundingBox | null => {\n    const width = maskImage.width;\n    const height = maskImage.height;\n    const canvas = document.createElement('canvas');\n    const ctx = canvas.getContext('2d');\n\n    if (!ctx) {\n      return null;\n    }\n\n    canvas.width = width;\n    canvas.height = height;\n    ctx.drawImage(maskImage, 0, 0, width, height);\n    const { data } = ctx.getImageData(0, 0, width, height);\n\n    let minX = width;\n    let minY = height;\n    let maxX = 0;\n    let maxY = 0;\n    let hasPixel = false;\n\n    for (let y = 0; y < height; y += 1) {\n      for (let x = 0; x < width; x += 1) {\n        const alpha = data[(y * width + x) * 4 + 3];\n        if (alpha > 25) {\n          hasPixel = true;\n          if (x < minX) minX = x;\n          if (y < minY) minY = y;\n          if (x > maxX) maxX = x;\n          if (y > maxY) maxY = y;\n        }\n      }\n    }\n\n    if (!hasPixel) {\n      return null;\n    }\n\n    return {\n      x: (minX / width) * 100,\n      y: (minY / height) * 100,\n      width: ((maxX - minX + 1) / width) * 100,\n      height: ((maxY - minY + 1) / height) * 100,\n    };\n  }, []);\n\n  const createSegmentLayers = useCallback(\n    async (\n      baseImageUrl: string,\n      items: Array<{ url?: string; data_url?: string }> = [],\n      isSegmentedImages: boolean = false,\n      masks: Array<{ url?: string; data_url?: string }> = []\n    ) => {\n      if (!baseImageUrl) return;\n\n      if (!items.length) {\n        setLayers((prev) => prev.filter((layer) => layer.kind !== 'segment'));\n        return;\n      }\n\n      // First, clear existing segment layers\n      setLayers((prev) => prev.filter((layer) => layer.kind !== 'segment'));\n\n      // Create or update the Background/Source layer\n      registerLayer(\n        {\n          name: 'Background',\n          kind: 'source',\n          preview: baseImageUrl,\n          visible: true,\n          metadata: {},\n        },\n        {\n          replaceKind: 'source',\n          autoSelect: false,\n        },\n      );\n\n      // Collect all segment layers first, then add them in one batch\n      const newSegmentLayers: Layer[] = [];\n      \n      for (let i = 0; i < items.length; i += 1) {\n        const item = items[i];\n        const itemSource = item?.data_url || item?.url;\n        \n        // Try to find corresponding mask\n        const maskItem = masks[i];\n        const maskSource = maskItem?.data_url || maskItem?.url || (isSegmentedImages ? undefined : itemSource);\n        \n        console.log(`[SEGMENT ${i + 1}] Item:`, {\n          hasDataUrl: !!item?.data_url,\n          hasUrl: !!item?.url,\n          hasMask: !!maskSource,\n          dataUrlStart: item?.data_url?.substring(0, 80),\n          urlStart: item?.url?.substring(0, 80)\n        });\n        if (!itemSource) {\n          console.warn(`[SEGMENT ${i + 1}] No source found, skipping`);\n          continue;\n        }\n        \n        try {\n          // If we have segmented_images, they're already the extracted objects\n          // If we have individual_masks, we need to apply them to the base image\n          let preview: string;\n          let boundingBox: BoundingBox | null = null;\n          \n          if (isSegmentedImages) {\n            // Already segmented - just use it directly\n            preview = itemSource;\n            const segmentedImage = await loadImageElement(itemSource);\n            boundingBox = extractBoundingBox(segmentedImage);\n          } else {\n            // It's a mask - apply it to the base image\n            console.log(`[SEGMENT ${i + 1}] Loading base and mask images...`);\n            const baseImage = await loadImageElement(baseImageUrl);\n            const maskImage = await loadImageElement(itemSource);\n            console.log(`[SEGMENT ${i + 1}] Loaded base: ${baseImage.width}x${baseImage.height}, mask: ${maskImage.width}x${maskImage.height}`);\n            console.log(`[SEGMENT ${i + 1}] Mask src hash:`, itemSource.substring(0, 100));\n            \n            // TEMP DEBUG: For the first and second segments, log details\n            if (i === 0 || i === 1) {\n              console.log(`[DEBUG] Segment ${i + 1} mask URL:`, itemSource.substring(0, 150));\n            }\n            preview = createMaskedPreview(baseImage, maskImage);\n            console.log(`[SEGMENT ${i + 1}] Preview created, length: ${preview.length}, start: ${preview.substring(0, 100)}`);\n            boundingBox = extractBoundingBox(maskImage);\n            console.log(`[SEGMENT ${i + 1}] Created preview, boundingBox:`, boundingBox);\n          }\n          \n          const layer: Layer = {\n            id: `segment-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 6)}`,\n            name: `Segment ${i + 1}`,\n            kind: 'segment',\n            preview,\n            visible: true,\n            timestamp: new Date().toLocaleTimeString(),\n            metadata: {\n              maskUrl: maskSource,\n              boundingBox,\n            },\n          };\n          \n          newSegmentLayers.push(layer);\n          console.log(`[SEGMENT ${i + 1}] Layer created with preview length: ${preview.length}`);\n        } catch (segmentError) {\n          console.error(`Failed to create segment layer ${i + 1}`, segmentError);\n        }\n      }\n\n      // Add all segment layers at once\n      if (newSegmentLayers.length > 0) {\n        setLayers((prev) => [...newSegmentLayers, ...prev]);\n        setSelectedLayerId(newSegmentLayers[0].id);\n        console.log(`[SEGMENTS] Added ${newSegmentLayers.length} segment layers in one batch`);\n      } else {\n        console.warn('[SEGMENTS] No segment layers were created');\n      }\n    },\n    [createMaskedPreview, extractBoundingBox, loadImageElement, registerLayer],\n  );\n\n  const handleAddAdjustmentLayer = useCallback(() => {\n    registerLayer({\n      name: `Adjustment ${adjustmentCount}`,\n      kind: 'empty',\n      preview: null,\n    });\n    setAdjustmentCount((count) => count + 1);\n  }, [adjustmentCount, registerLayer]);\n\n  useEffect(() => {\n    if (!layers.length) {\n      setSelectedLayerId(null);\n      return;\n    }\n    const exists = layers.some((layer) => layer.id === selectedLayerId);\n    if (!exists) {\n      setSelectedLayerId(layers[0].id);\n    }\n  }, [layers, selectedLayerId]);\n\n  const activeLayer = selectedLayerId\n    ? layers.find((layer) => layer.id === selectedLayerId) ?? layers[0]\n    : layers[0];\n  const displayedImage = activeLayer?.preview || editedImage || image;\n  const canCrop = Boolean(displayedImage);\n  const segmentLayers = layers.filter((layer) => layer.kind === 'segment');\n  const baseImageForSegments = editedImage || image || null;\n  const sourceLayer = layers.find((layer) => layer.kind === 'source');\n  const showBaseImage = segmentLayers.length === 0 || !sourceLayer || sourceLayer.visible !== false;\n  const historyEntries = [\n    'Session started',\n    image ? 'Image imported' : null,\n    editedImage ? 'AI edit applied' : null,\n  ].filter((entry): entry is string => Boolean(entry));\n\n  const baseLayer = useMemo(() => {\n    return layers.find((layer) => layer.kind === 'ai') || layers.find((layer) => layer.kind === 'source') || null;\n  }, [layers]);\n\n  const cropTargetLayer = useMemo(() => {\n    if (cropTargetLayerId) {\n      return layers.find((layer) => layer.id === cropTargetLayerId) || null;\n    }\n    if (activeLayer?.kind === 'segment') {\n      return baseLayer;\n    }\n    return activeLayer ?? baseLayer;\n  }, [activeLayer, baseLayer, cropTargetLayerId, layers]);\n\n  const cropperImage = cropTargetLayer?.preview || baseImageForSegments || displayedImage || null;\n  const hasEditableImage = Boolean(image || editedImage);\n\n  const handleAutoSegment = useCallback(async () => {\n    const baseImageSource = image || editedImage;\n    if (!baseImageSource) return;\n\n    setIsSegmenting(true);\n    setError(null);\n\n    try {\n      const blob = await dataUrlToBlob(baseImageSource);\n      const segmentationData = await segmentImage(blob);\n      \n      // Debug: Log what we received\n      console.log('[SEGMENT] Received data:', {\n        hasIndividualMasks: !!segmentationData?.individual_masks,\n        individualMasksCount: segmentationData?.individual_masks?.length,\n        hasSegmentedImages: !!segmentationData?.segmented_images,\n        segmentedImagesCount: segmentationData?.segmented_images?.length,\n        keys: Object.keys(segmentationData || {})\n      });\n      \n      // Check if we should use segmented_images (pre-extracted objects) or individual_masks (need to apply to base)\n      const hasSegmentedImages = segmentationData?.segmented_images && segmentationData.segmented_images.length > 0;\n      const itemsToUse = hasSegmentedImages \n        ? segmentationData.segmented_images \n        : (segmentationData?.individual_masks || []);\n      const masksToUse = segmentationData?.individual_masks || [];\n      \n      console.log('[SEGMENT] Using:', hasSegmentedImages ? 'segmented_images' : 'individual_masks', itemsToUse.length);\n      \n      await createSegmentLayers(baseImageSource, itemsToUse, hasSegmentedImages, masksToUse);\n    } catch (err) {\n      const errorMessage = err instanceof Error ? err.message : 'Segmentation failed';\n      setError(errorMessage);\n      console.error('Segmentation error:', err);\n    } finally {\n      setIsSegmenting(false);\n    }\n  }, [createSegmentLayers, dataUrlToBlob, image, editedImage]);\n\n  const handleToolSelect = useCallback(\n    (toolId: string) => {\n      setActiveTool(toolId);\n      if (toolId === 'crop' && canCrop) {\n        const targetLayer =\n          activeLayer && activeLayer.kind !== 'segment' ? activeLayer : baseLayer;\n        if (targetLayer) {\n          if (selectedLayerId !== targetLayer.id) {\n            setSelectedLayerId(targetLayer.id);\n          }\n          setCropTargetLayerId(targetLayer.id);\n        } else {\n          setCropTargetLayerId(null);\n        }\n        setShowCropper(true);\n      } else {\n        setShowCropper(false);\n        setCropTargetLayerId(null);\n      }\n      if (toolId === 'magic' && image) {\n        handleAutoSegment();\n      }\n    },\n    [activeLayer, baseLayer, canCrop, handleAutoSegment, image, selectedLayerId],\n  );\n\n\n  useEffect(() => {\n    if (activeTool === 'crop' && canCrop) {\n      setShowCropper(true);\n    } else {\n      setShowCropper(false);\n    }\n  }, [activeTool, canCrop]);\n\n  useEffect(() => {\n    if (showCropper) {\n      setCrop({ x: 0, y: 0 });\n      setZoom(1);\n      setCroppedAreaPixels(null);\n    } else {\n      setCroppedAreaPixels(null);\n      setCropTargetLayerId(null);\n    }\n  }, [showCropper]);\n\n  const onCropComplete = useCallback((_: Area, croppedArea: Area) => {\n    setCroppedAreaPixels(croppedArea);\n  }, []);\n\n  const getCroppedImage = useCallback(\n    (imageSrc: string, croppedArea: Area) =>\n      new Promise<string>((resolve, reject) => {\n        const imageElement = new Image();\n        imageElement.src = imageSrc;\n        imageElement.crossOrigin = 'anonymous';\n        imageElement.onload = () => {\n          const canvas = document.createElement('canvas');\n          canvas.width = croppedArea.width;\n          canvas.height = croppedArea.height;\n          const ctx = canvas.getContext('2d');\n          if (!ctx) {\n            reject(new Error('Canvas not supported'));\n            return;\n          }\n          ctx.drawImage(\n            imageElement,\n            croppedArea.x,\n            croppedArea.y,\n            croppedArea.width,\n            croppedArea.height,\n            0,\n            0,\n            croppedArea.width,\n            croppedArea.height,\n          );\n          resolve(canvas.toDataURL('image/png'));\n        };\n        imageElement.onerror = () => reject(new Error('Failed to load image for cropping'));\n      }),\n    [],\n  );\n\n  const handleApplyCrop = useCallback(async () => {\n    if (!croppedAreaPixels) return;\n\n    const targetLayer = cropTargetLayer || baseLayer;\n    const targetImage = targetLayer?.preview || baseImageForSegments || displayedImage || editedImage || image;\n\n    if (!targetImage) return;\n\n    try {\n      const croppedDataUrl = await getCroppedImage(targetImage, croppedAreaPixels);\n      const targetKind: LayerKind = targetLayer?.kind ?? (editedImage ? 'ai' : 'source');\n      const shouldResetSegments = targetKind === 'source' || targetKind === 'ai';\n\n      setLayers((prev) => {\n        let updated = targetLayer\n          ? prev.map((layer) =>\n              layer.id === targetLayer.id\n                ? { ...layer, preview: croppedDataUrl, timestamp: new Date().toLocaleTimeString() }\n                : layer,\n            )\n          : prev;\n\n        if (shouldResetSegments) {\n          updated = updated.filter((layer) => layer.kind !== 'segment');\n        }\n\n        return updated;\n      });\n\n      if (targetKind === 'source') {\n        setImage(croppedDataUrl);\n      } else if (targetKind === 'ai') {\n        setEditedImage(croppedDataUrl);\n      } else if (!targetLayer) {\n        if (editedImage) {\n          setEditedImage(croppedDataUrl);\n        } else {\n          setImage(croppedDataUrl);\n        }\n      }\n\n      if (targetLayer) {\n        setSelectedLayerId(targetLayer.id);\n      }\n\n      setShowCropper(false);\n      setActiveTool('select');\n      setCropTargetLayerId(null);\n    } catch (cropError) {\n      console.error(cropError);\n      setError('Failed to crop image. Please try again.');\n    }\n  }, [\n    baseImageForSegments,\n    baseLayer,\n    cropTargetLayer,\n    croppedAreaPixels,\n    displayedImage,\n    editedImage,\n    getCroppedImage,\n    image,\n  ]);\n\n  const handleCancelCrop = () => {\n    setShowCropper(false);\n    setActiveTool('select');\n    setCropTargetLayerId(null);\n  };\n\n  const handleLayerSelect = (layerId: string) => {\n    setSelectedLayerId(layerId);\n    \n    // TEMP: Log which layer was selected for debugging\n    const layer = layers.find(l => l.id === layerId);\n    console.log('[LAYER SELECT]', layer?.name, 'preview length:', layer?.preview?.length);\n    if (layer?.preview) {\n      console.log('[LAYER SELECT] Preview URL (open in new tab):', layer.preview);\n    }\n  };\n\n  const toggleLayerVisibility = useCallback((layerId: string, soloMode: boolean = false) => {\n    setLayers((prev) => {\n      if (soloMode) {\n        // Solo mode: hide all segments except this one\n        return prev.map((layer) => {\n          if (layer.kind === 'segment') {\n            return { ...layer, visible: layer.id === layerId };\n          }\n          return layer;\n        });\n      } else {\n        // Normal toggle\n        return prev.map((layer) =>\n          layer.id === layerId ? { ...layer, visible: layer.visible === false ? true : false } : layer,\n        );\n      }\n    });\n  }, []);\n\n  const handlePresetApply = (preset: string) => {\n    setPrompt(preset);\n  };\n\n  const handleAspectPreset = (ratio: number | undefined) => {\n    setAspect(ratio);\n  };\n\n  const handleApplyFilter = async (filterPrompt: string) => {\n    if (!image) {\n      setError('Please upload an image first');\n      return;\n    }\n\n    if (!apiConfigured) {\n      setError('API key not configured. Please check backend configuration.');\n      return;\n    }\n\n    setIsLoading(true);\n    setError(null);\n    setPrompt(filterPrompt);\n\n    try {\n      const data = await editImage({\n        image: await dataUrlToBlob(image),\n        prompt: filterPrompt.trim(),\n        negativePrompt: negativePrompt.trim() || undefined,\n        model,\n      });\n\n      const imageUrl = extractImageUrl(data);\n      if (imageUrl) {\n        setEditedImage(imageUrl);\n        registerLayer({\n          name: `Filter: ${filterPrompt.trim().substring(0, 15)}...`,\n          kind: 'ai',\n          preview: imageUrl,\n        });\n        setAiLayerCount((c) => c + 1);\n      }\n    } catch (err) {\n      const errorMessage = err instanceof Error ? err.message : 'Filter application failed';\n      setError(errorMessage);\n      console.error('Filter error:', err);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleQuickEdit = async (action: QuickEditAction) => {\n    const baseSource = editedImage || image;\n    if (!baseSource) {\n      setError('Please upload an image first');\n      return;\n    }\n\n    if (!apiConfigured) {\n      setError('API key not configured. Please check backend configuration.');\n      return;\n    }\n\n    setIsLoading(true);\n    setError(null);\n    setPrompt(action.prompt);\n\n    try {\n      const blob = await dataUrlToBlob(baseSource);\n      const data = await editImage({\n        image: blob,\n        prompt: action.prompt.trim(),\n        negativePrompt: action.negativePrompt?.trim() || undefined,\n        model,\n      });\n\n      const imageUrl = extractImageUrl(data);\n      if (imageUrl) {\n        setEditedImage(imageUrl);\n        registerLayer({\n          name: `Quick: ${action.label}`,\n          kind: 'ai',\n          preview: imageUrl,\n        });\n        setAiLayerCount((count) => count + 1);\n      }\n    } catch (err) {\n      const errorMessage = err instanceof Error ? err.message : 'Quick edit failed';\n      setError(errorMessage);\n      console.error('Quick edit error:', err);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleMenuClick = (menuItem: string) => {\n    switch (menuItem) {\n      case 'Export':\n        handleDownload();\n        break;\n      case 'Help':\n        window.open('https://github.com/your-repo/nanobanana-studio', '_blank');\n        break;\n      default:\n        break;\n    }\n  };\n\n  // Check API health on mount\n  useEffect(() => {\n    checkHealth()\n      .then((health) => {\n        setApiConfigured(health.apiConfigured);\n        if (!health.apiConfigured) {\n          setError('API key not configured. Please set FAL_API_KEY in backend/.env');\n        }\n      })\n      .catch(() => {\n        setError('Unable to connect to backend server');\n      });\n  }, []);\n\n  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    // Validate file\n    const validationError = validateImageFile(file);\n    if (validationError) {\n      setError(validationError);\n      return;\n    }\n\n    // Read file\n    const reader = new FileReader();\n    reader.onload = (event) => {\n      const dataUrl = event.target?.result as string;\n      setImage(dataUrl);\n      setEditedImage(null);\n      setError(null);\n      registerLayer(\n        {\n          name: 'Source Asset',\n          kind: 'source',\n          preview: dataUrl,\n        },\n        { replaceKind: 'source' },\n      );\n      setAiLayerCount(0);\n    };\n    reader.onerror = () => {\n      setError('Failed to read image file');\n    };\n    reader.readAsDataURL(file);\n  };\n\n  const handleProcess = async () => {\n    // Validate inputs\n    if (!prompt.trim()) {\n      setError('Please enter a prompt');\n      return;\n    }\n\n    if (prompt.length > 2000) {\n      setError('Prompt is too long (max 2000 characters)');\n      return;\n    }\n\n    if (mode === 'edit' && !image) {\n      setError('Please upload an image first');\n      return;\n    }\n\n    if (!apiConfigured) {\n      setError('API key not configured. Please check backend configuration.');\n      return;\n    }\n\n    setIsLoading(true);\n    setError(null);\n\n    try {\n      let data;\n\n      if (mode === 'edit') {\n        // Convert data URL to blob\n        const blob = await dataUrlToBlob(image!);\n\n        // Call edit API\n        data = await editImage({\n          image: blob,\n          prompt: prompt.trim(),\n          negativePrompt: negativePrompt.trim() || undefined,\n          model,\n        });\n      } else {\n        // Call generate API\n        data = await generateImage({\n          prompt: prompt.trim(),\n          negativePrompt: negativePrompt.trim() || undefined,\n          model,\n        });\n      }\n\n      // Extract image URL from response\n      const imageUrl = extractImageUrl(data);\n\n      if (imageUrl) {\n        setEditedImage(imageUrl);\n        const layerName = mode === 'edit' ? `AI Output ${aiLayerCount + 1}` : `Generation ${aiLayerCount + 1}`;\n        registerLayer({\n          name: layerName,\n          kind: 'ai',\n          preview: imageUrl,\n        });\n        setAiLayerCount((count) => count + 1);\n      } else {\n        throw new Error('No image in response. Please try again.');\n      }\n    } catch (err) {\n      const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred';\n      setError(errorMessage);\n      console.error('Error processing image:', err);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleDownload = () => {\n    if (editedImage) {\n      const filename = `nanobanana-${mode}-${getTimestamp()}.png`;\n      downloadImage(editedImage, filename);\n    }\n  };\n\n  const handleClearImage = () => {\n    setImage(null);\n    setEditedImage(null);\n    setError(null);\n    if (fileInputRef.current) {\n      fileInputRef.current.value = '';\n    }\n  };\n\n  const handleModeChange = (newMode: Mode) => {\n    setMode(newMode);\n    setError(null);\n  };\n\n  const handleKeyPress = (e: React.KeyboardEvent) => {\n    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n      handleProcess();\n    }\n  };\n\n  return (\n    <div className=\"app\">\n      <header className=\"top-bar\">\n        <div className=\"brand\">\n          <Sparkles className=\"brand-icon\" size={20} />\n          <div>\n            <p className=\"brand-title\">NanoBanana Studio</p>\n            <span className=\"brand-subtitle\">NanoBanana & Pro • fal.ai</span>\n          </div>\n        </div>\n\n        <div className=\"quick-action-nav\">\n          {quickEdits.map((action) => (\n            <button\n              key={action.label}\n              type=\"button\"\n              className=\"quick-action-icon-btn\"\n              onClick={() => handleQuickEdit(action)}\n              disabled={!hasEditableImage || isLoading}\n              title={action.description}\n            >\n              <action.icon size={16} />\n              <span>{action.label}</span>\n            </button>\n          ))}\n        </div>\n\n        <nav className=\"menu-strip\">\n          {menuItems.map((label) => (\n            <button\n              key={label}\n              className=\"menu-item\"\n              type=\"button\"\n              onClick={() => handleMenuClick(label)}\n            >\n              {label}\n            </button>\n          ))}\n        </nav>\n\n        <div className=\"status-cluster\">\n          <span className={`status-dot ${apiConfigured ? 'online' : 'offline'}`} />\n          <span>{apiConfigured ? 'Connected' : 'API Offline'}</span>\n          <button className=\"secondary-btn\" type=\"button\">\n            <Settings2 size={16} />\n            Studio prefs\n          </button>\n        </div>\n      </header>\n\n      <div className=\"workspace\">\n        <aside className=\"tool-panel\">\n          {toolset.map((tool) => (\n            <button\n              key={tool.id}\n              className={`tool-btn ${activeTool === tool.id ? 'active' : ''}`}\n              onClick={() => handleToolSelect(tool.id)}\n              disabled={tool.id === 'crop' && !canCrop}\n              type=\"button\"\n            >\n              <tool.icon size={18} />\n              <span>{tool.label}</span>\n            </button>\n          ))}\n        </aside>\n\n        <section className=\"canvas-shell\">\n          <div className=\"options-bar\">\n            <div>\n              <p className=\"document-title\">\n                {image ? 'canvas.png' : 'Untitled canvas'}\n              </p>\n              <span className=\"document-subtitle\">\n                {mode === 'edit' ? 'Layered Edit Session' : 'Generative Session'}\n              </span>\n            </div>\n            <div className=\"mode-toggle chips\">\n              <button\n                className={`mode-chip ${mode === 'edit' ? 'active' : ''}`}\n                onClick={() => handleModeChange('edit')}\n                disabled={isLoading}\n                type=\"button\"\n              >\n                <ImageIcon size={16} />\n                Edit\n              </button>\n              <button\n                className={`mode-chip ${mode === 'generate' ? 'active' : ''}`}\n                onClick={() => handleModeChange('generate')}\n                disabled={isLoading}\n                type=\"button\"\n              >\n                <Wand2 size={16} />\n                Generate\n              </button>\n            </div>\n            <div className=\"model-toggle chips\">\n              {modelOptions.map((option) => (\n                <button\n                  key={option.id}\n                  className={`model-chip ${model === option.id ? 'active' : ''}`}\n                  onClick={() => setModel(option.id)}\n                  type=\"button\"\n                  disabled={isLoading}\n                >\n                  <strong>{option.label}</strong>\n                  <span>{option.description}</span>\n                </button>\n              ))}\n            </div>\n          </div>\n\n          <div className=\"canvas-stage\">\n            <div className=\"canvas-area pro\">\n              {!displayedImage ? (\n                mode === 'edit' ? (\n                  <div className=\"empty-state\">\n                    <ImageIcon size={64} />\n                    <p>Edit Images with AI</p>\n                    <span className=\"hint\">Supports JPEG, PNG, and WebP (max 50MB)</span>\n                  </div>\n                ) : (\n                  <div className=\"empty-state\">\n                    <Wand2 size={64} />\n                    <p>Describe the scene you need</p>\n                    <span className=\"hint\">Press Ctrl/Cmd + Enter to generate</span>\n                  </div>\n                )\n              ) : (\n                <div className={`image-container framed ${showCropper ? 'cropping' : ''}`}>\n                  {showCropper && cropperImage ? (\n                    <div className=\"cropper-wrapper\">\n                      <Cropper\n                        image={cropperImage}\n                        crop={crop}\n                        zoom={zoom}\n                        aspect={aspect}\n                        onCropChange={setCrop}\n                        onZoomChange={setZoom}\n                        onCropComplete={onCropComplete}\n                      />\n                    </div>\n                  ) : (\n                    <div className=\"canvas-stack\">\n                      {baseImageForSegments && (\n                        <img\n                          src={baseImageForSegments}\n                          alt=\"Canvas preview\"\n                          className=\"result-image\"\n                          style={{\n                            visibility: showBaseImage ? 'visible' : 'hidden',\n                          }}\n                        />\n                      )}\n                      {segmentLayers.length > 0 && (\n                        <div className=\"segmentation-overlay\">\n                          {(() => {\n                            const visibleCount = segmentLayers.filter(l => l.visible !== false).length;\n                            console.log(`[RENDER] Rendering ${visibleCount} visible segments out of ${segmentLayers.length} total`);\n                            return null;\n                          })()}\n                          {segmentLayers.map((layer, idx) => {\n                            if (!layer.preview) {\n                              console.warn(`[RENDER] Layer ${layer.name} has no preview`);\n                              return null;\n                            }\n                            const isVisible = layer.visible !== false;\n                            const isSelected = layer.id === activeLayer?.id;\n                            \n                            // Debug first layer\n                            if (idx === 0) {\n                              console.log(`[RENDER] First segment layer:`, {\n                                name: layer.name,\n                                isVisible,\n                                isSelected,\n                                previewLength: layer.preview.length,\n                                previewStart: layer.preview.substring(0, 50)\n                              });\n                            }\n                            \n                            return (\n                              <div\n                                key={layer.id}\n                                style={{\n                                  position: 'absolute',\n                                  top: 0,\n                                  left: 0,\n                                  right: 0,\n                                  bottom: 0,\n                                  display: isVisible ? 'flex' : 'none',\n                                  alignItems: 'center',\n                                  justifyContent: 'center',\n                                  opacity: !isVisible ? 0 : (isSelected ? 1 : 0.8),\n                                  pointerEvents: isVisible ? 'auto' : 'none',\n                                }}\n                                onClick={() => isVisible && handleLayerSelect(layer.id)}\n                              >\n                                <img\n                                  src={layer.preview}\n                                  alt={layer.name}\n                                  className={`segment-mask ${isSelected ? 'selected' : ''}`}\n                                  style={{\n                                    maxWidth: '100%',\n                                    maxHeight: '100%',\n                                    objectFit: 'contain',\n                                  }}\n                                  onLoad={(e) => {\n                                    const img = e.target as HTMLImageElement;\n                                    console.log(`[RENDER] ${layer.name} loaded: ${img.naturalWidth}x${img.naturalHeight}, displayed: ${img.width}x${img.height}`);\n                                  }}\n                                  onError={(e) => console.error(`[RENDER] Image failed to load: ${layer.name}`, e)}\n                                />\n                              </div>\n                            );\n                          })}\n                          {activeLayer?.kind === 'segment' && activeLayer.metadata?.boundingBox && (\n                            <div\n                              className=\"selection-box\"\n                              style={{\n                                left: `${activeLayer.metadata.boundingBox.x}%`,\n                                top: `${activeLayer.metadata.boundingBox.y}%`,\n                                width: `${activeLayer.metadata.boundingBox.width}%`,\n                                height: `${activeLayer.metadata.boundingBox.height}%`,\n                              }}\n                            />\n                          )}\n                        </div>\n                      )}\n                      {isSegmenting && (\n                        <div className=\"segmentation-loading\">\n                          <Loader2 className=\"spinner\" size={40} />\n                          <span>Segmenting objects...</span>\n                        </div>\n                      )}\n                      {editedImage && (\n                        <div className=\"image-actions floating\">\n                          <button onClick={handleDownload} className=\"download-btn\" type=\"button\">\n                            <Download size={18} />\n                            Export PNG\n                          </button>\n                        </div>\n                      )}\n                    </div>\n                  )}\n                </div>\n              )}\n            </div>\n\n            <div className=\"stack-panels\">\n              <div className=\"panel-card subtle\">\n                <div className=\"panel-header\">\n                  <div className=\"panel-header-info\">\n                    <Layers size={16} />\n                    <span>Layers</span>\n                  </div>\n                  <button className=\"layer-add-btn\" type=\"button\" onClick={handleAddAdjustmentLayer}>\n                    <Plus size={14} />\n                    New layer\n                  </button>\n                </div>\n                <div className=\"layer-list\">\n                  {layers.length === 0 ? (\n                    <div className=\"layer-item muted\">Layers will appear here</div>\n                  ) : (\n                    layers.map((layer) => (\n                      <button\n                        key={layer.id}\n                        type=\"button\"\n                        onClick={() => handleLayerSelect(layer.id)}\n                        className={`layer-item ${\n                          layer.id === activeLayer?.id ? 'active' : ''\n                        } ${!layer.preview ? 'muted' : ''} ${layer.visible === false ? 'hidden' : ''}`}\n                      >\n                        <div className=\"layer-meta\">\n                          <strong>{layer.name}</strong>\n                          <span>{layer.timestamp}</span>\n                        </div>\n                        <div className=\"layer-actions\">\n                          <span className={`layer-pill ${layer.kind}`}>{layer.kind}</span>\n                          {layer.kind === 'segment' && layer.preview && (\n                            <>\n                              <button\n                                type=\"button\"\n                                className=\"layer-eye-btn\"\n                                onClick={(event) => {\n                                  event.stopPropagation();\n                                  // Download this segment\n                                  if (layer.preview) {\n                                    const link = document.createElement('a');\n                                    link.href = layer.preview;\n                                    link.download = `${layer.name}.png`;\n                                    link.click();\n                                  }\n                                }}\n                                aria-label=\"Download segment\"\n                                title=\"Download segment\"\n                              >\n                                <Download size={14} />\n                              </button>\n                              <button\n                                type=\"button\"\n                                className=\"layer-eye-btn\"\n                                onClick={(event) => {\n                                  event.stopPropagation();\n                                  toggleLayerVisibility(layer.id, event.altKey);\n                                }}\n                                aria-label={layer.visible === false ? 'Show layer' : 'Hide layer'}\n                                title={layer.visible === false ? 'Show layer (Alt+Click to solo)' : 'Hide layer (Alt+Click to solo)'}\n                              >\n                                {layer.visible === false ? <EyeOff size={14} /> : <Eye size={14} />}\n                              </button>\n                            </>\n                          )}\n                          {layer.kind === 'source' && (\n                            <button\n                              type=\"button\"\n                              className=\"layer-eye-btn\"\n                              onClick={(event) => {\n                                event.stopPropagation();\n                                toggleLayerVisibility(layer.id);\n                              }}\n                              aria-label={layer.visible === false ? 'Show background' : 'Hide background'}\n                            >\n                              {layer.visible === false ? <EyeOff size={14} /> : <Eye size={14} />}\n                            </button>\n                          )}\n                        </div>\n                      </button>\n                    ))\n                  )}\n                </div>\n              </div>\n\n              <div className=\"panel-card subtle\">\n                <div className=\"panel-header\">\n                  <History size={16} />\n                  <span>History</span>\n                </div>\n                <ul className=\"history-list\">\n                  {historyEntries.map((entry) => (\n                    <li key={entry}>{entry}</li>\n                  ))}\n                </ul>\n              </div>\n            </div>\n          </div>\n        </section>\n\n        <aside className=\"control-panel\">\n          <div className=\"panel-card\">\n            <div className=\"panel-header\">\n              <h2>{mode === 'edit' ? 'Edit Controls' : 'Generation Controls'}</h2>\n            </div>\n\n            {mode === 'edit' && (\n              <div className=\"upload-deck\">\n                <input\n                  ref={fileInputRef}\n                  type=\"file\"\n                  accept=\"image/jpeg,image/png,image/jpg,image/webp\"\n                  onChange={handleImageUpload}\n                  className=\"file-input\"\n                  id=\"file-input\"\n                  disabled={isLoading}\n                />\n                <label htmlFor=\"file-input\" className={`upload-btn ${isLoading ? 'disabled' : ''}`}>\n                  <Upload size={20} />\n                  {image ? 'Replace Asset' : 'Import Image'}\n                </label>\n                {image && (\n                  <button\n                    className=\"clear-btn ghost\"\n                    onClick={handleClearImage}\n                    disabled={isLoading}\n                    type=\"button\"\n                  >\n                    <X size={16} />\n                    Remove Layer\n                  </button>\n                )}\n              </div>\n            )}\n\n            <div className=\"prompt-section\">\n              <div className=\"prompt-header\">\n                <label htmlFor=\"prompt\">\n                  {mode === 'edit' ? 'Editing prompt' : 'Generation prompt'}\n                </label>\n                <span className=\"char-count\">{prompt.length}/2000</span>\n              </div>\n              <textarea\n                id=\"prompt\"\n                value={prompt}\n                onChange={(e) => setPrompt(e.target.value)}\n                onKeyDown={handleKeyPress}\n                placeholder={\n                  mode === 'edit'\n                    ? 'Relight the subject with golden hour tones...'\n                    : 'Ultra-wide hero shot of a desert city at dusk...'\n                }\n                rows={5}\n                className=\"prompt-input\"\n                disabled={isLoading}\n                maxLength={2000}\n              />\n            </div>\n\n            <div className=\"prompt-section\">\n              <label htmlFor=\"negative-prompt\">Negative prompt</label>\n              <textarea\n                id=\"negative-prompt\"\n                value={negativePrompt}\n                onChange={(e) => setNegativePrompt(e.target.value)}\n                placeholder=\"Artifacts to avoid (e.g., blur, watermark, distortion)\"\n                rows={3}\n                className=\"prompt-input\"\n                disabled={isLoading}\n              />\n            </div>\n\n            {activeTool === 'crop' && canCrop && (\n              <div className=\"crop-controls\">\n                <div className=\"crop-header\">\n                  <div>\n                    <p>Crop controls</p>\n                    <span>Use handles directly on canvas</span>\n                  </div>\n                </div>\n                <div className=\"aspect-options\">\n                  {aspectPresets.map((preset) => {\n                    const isActive = preset.ratio ? aspect === preset.ratio : !aspect;\n                    return (\n                      <button\n                        key={preset.label}\n                        type=\"button\"\n                        className={`aspect-chip ${isActive ? 'active' : ''}`}\n                        onClick={() => handleAspectPreset(preset.ratio)}\n                      >\n                        {preset.label}\n                      </button>\n                    );\n                  })}\n                </div>\n                <label className=\"crop-label\" htmlFor=\"crop-zoom\">\n                  Zoom\n                </label>\n                <input\n                  id=\"crop-zoom\"\n                  type=\"range\"\n                  min={1}\n                  max={3}\n                  step={0.05}\n                  value={zoom}\n                  onChange={(e) => setZoom(Number(e.target.value))}\n                  className=\"crop-slider\"\n                />\n                <div className=\"crop-actions\">\n                  <button className=\"clear-btn ghost\" type=\"button\" onClick={handleCancelCrop}>\n                    Cancel\n                  </button>\n                  <button\n                    className=\"primary-btn\"\n                    type=\"button\"\n                    onClick={handleApplyCrop}\n                    disabled={!croppedAreaPixels}\n                  >\n                    Apply crop\n                  </button>\n                </div>\n              </div>\n            )}\n\n            {activeTool === 'filters' && (\n              <div className=\"filters-panel\">\n                <div className=\"panel-header\">\n                  <h2>Photo Filters</h2>\n                  <span>Apply instant AI filters to your image</span>\n                </div>\n\n                <div className=\"filters-grid\">\n                  {filterPresets.map((filter) => (\n                    <button\n                      key={filter.label}\n                      type=\"button\"\n                      className=\"filter-chip\"\n                      onClick={() => handleApplyFilter(filter.prompt)}\n                      title={filter.prompt}\n                    >\n                      <div className=\"filter-label\">{filter.label}</div>\n                      <div className=\"filter-category\">{filter.category}</div>\n                    </button>\n                  ))}\n                </div>\n\n                <div className=\"filter-instructions\">\n                  <p>Click any filter to instantly apply it to your image using AI. Each filter uses carefully crafted prompts for professional results.</p>\n                </div>\n              </div>\n            )}\n\n            <div className=\"preset-grid\">\n              {presetPrompts.map((preset) => (\n                <button\n                  key={preset.label}\n                  type=\"button\"\n                  className=\"preset-chip\"\n                  onClick={() => handlePresetApply(preset.prompt)}\n                  title={preset.prompt}\n                >\n                  {preset.label}\n                </button>\n              ))}\n            </div>\n\n\n            <button\n              onClick={handleProcess}\n              disabled={isLoading || (mode === 'edit' && !image) || !prompt.trim()}\n              className=\"edit-btn\"\n              title=\"Ctrl/Cmd + Enter\"\n              type=\"button\"\n            >\n              {isLoading ? (\n                <>\n                  <Loader2 className=\"spinner\" size={20} />\n                  Processing...\n                </>\n              ) : (\n                <>\n                  <Sparkles size={20} />\n                  {mode === 'edit' ? 'Run Edit' : 'Generate Image'}\n                </>\n              )}\n            </button>\n\n            {error && (\n              <div className=\"error-message\">\n                <strong>Error:</strong> {error}\n              </div>\n            )}\n\n            {!apiConfigured && !error && (\n              <div className=\"warning-message\">⚠️ API key not configured</div>\n            )}\n          </div>\n        </aside>\n      </div>\n\n      <footer className=\"status-bar\">\n        <span>Zoom 100%</span>\n        <span>Document RGB • 16-bit</span>\n        <span>{isLoading ? 'Working...' : 'Idle'}</span>\n      </footer>\n    </div>\n  );\n}\n\nexport default App;\n"
  },
  {
    "path": "frontend/src/api.ts",
    "content": "import { EditResponse, EditImageParams, GenerateImageParams, InpaintImageParams } from './types';\n\nconst API_BASE = '/api';\nconst DEFAULT_MODEL = 'nano';\n\nasync function handleApiResponse<T>(response: Response): Promise<T> {\n  if (!response.ok) {\n    const errorData = await response.json().catch(() => ({ error: 'Failed to process request' }));\n    throw new Error(errorData.error || `Server error: ${response.status}`);\n  }\n  return response.json();\n}\n\nexport async function editImage(params: EditImageParams): Promise<EditResponse> {\n  const formData = new FormData();\n  formData.append('image', params.image, 'image.png');\n  formData.append('prompt', params.prompt);\n  \n  if (params.negativePrompt) {\n    formData.append('negativePrompt', params.negativePrompt);\n  }\n  formData.append('model', params.model ?? DEFAULT_MODEL);\n\n  const response = await fetch(`${API_BASE}/edit-image`, {\n    method: 'POST',\n    body: formData,\n  });\n\n  return handleApiResponse<EditResponse>(response);\n}\n\nexport async function inpaintImage(params: InpaintImageParams): Promise<EditResponse> {\n  const formData = new FormData();\n  \n  if (typeof params.image === 'string') {\n    formData.append('imageUrl', params.image);\n  } else {\n    formData.append('image', params.image, 'image.png');\n  }\n\n  if (typeof params.mask === 'string') {\n    formData.append('maskUrl', params.mask);\n  } else {\n    formData.append('mask', params.mask, 'mask.png');\n  }\n\n  formData.append('prompt', params.prompt);\n\n  const response = await fetch(`${API_BASE}/inpaint-image`, {\n    method: 'POST',\n    body: formData,\n  });\n\n  return handleApiResponse<EditResponse>(response);\n}\n\nexport async function generateImage(params: GenerateImageParams): Promise<EditResponse> {\n  const response = await fetch(`${API_BASE}/generate-image`, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n      prompt: params.prompt,\n      negativePrompt: params.negativePrompt || undefined,\n      width: params.width,\n      height: params.height,\n      model: params.model ?? DEFAULT_MODEL,\n    }),\n  });\n\n  return handleApiResponse<EditResponse>(response);\n}\n\nexport async function segmentImage(image: Blob): Promise<any> {\n  const formData = new FormData();\n  formData.append('image', image, 'image.png');\n\n  const response = await fetch(`${API_BASE}/segment-image`, {\n    method: 'POST',\n    body: formData,\n  });\n\n  return handleApiResponse<any>(response);\n}\n\nexport async function checkHealth(): Promise<{ status: string; apiConfigured: boolean }> {\n  const response = await fetch(`${API_BASE}/health`);\n  return handleApiResponse(response);\n}\n"
  },
  {
    "path": "frontend/src/index.css",
    "content": "* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  background: #1a1a1a;\n  color: #e0e0e0;\n  overflow: hidden;\n}\n\n#root {\n  width: 100vw;\n  height: 100vh;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}\n\n"
  },
  {
    "path": "frontend/src/main.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './App';\nimport './index.css';\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n);\n\n"
  },
  {
    "path": "frontend/src/react-easy-crop.d.ts",
    "content": "declare module 'react-easy-crop' {\n  import { ComponentType } from 'react';\n\n  export interface Area {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  }\n\n  export interface CropCoordinates {\n    x: number;\n    y: number;\n  }\n\n  export interface CropperProps {\n    image?: string;\n    crop?: CropCoordinates;\n    zoom?: number;\n    aspect?: number;\n    onCropChange?: (value: CropCoordinates) => void;\n    onZoomChange?: (value: number) => void;\n    onCropComplete?: (croppedArea: Area, croppedAreaPixels: Area) => void;\n    restrictPosition?: boolean;\n  }\n\n  const Cropper: ComponentType<CropperProps>;\n  export default Cropper;\n}\n\n"
  },
  {
    "path": "frontend/src/types.ts",
    "content": "export interface EditResponse {\n  images?: Array<{ url: string }>;\n  image?: { url: string };\n}\n\nexport interface ApiError {\n  error: string;\n  details?: string;\n  timestamp?: string;\n}\n\nexport type Mode = 'edit' | 'generate';\n\nexport type ModelId = 'nano' | 'pro';\n\nexport interface EditImageParams {\n  image: Blob;\n  prompt: string;\n  negativePrompt?: string;\n  model?: ModelId;\n}\n\nexport interface InpaintImageParams {\n  image: Blob | string;\n  mask: Blob | string;\n  prompt: string;\n}\n\nexport interface GenerateImageParams {\n  prompt: string;\n  negativePrompt?: string;\n  width?: number;\n  height?: number;\n  model?: ModelId;\n}\n"
  },
  {
    "path": "frontend/src/utils.ts",
    "content": "import { EditResponse } from './types';\n\nconst MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB\nconst ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp'];\n\nexport function validateImageFile(file: File): string | null {\n  if (!ALLOWED_FILE_TYPES.includes(file.type)) {\n    return 'Please upload a valid image file (JPEG, PNG, or WebP)';\n  }\n\n  if (file.size > MAX_FILE_SIZE) {\n    return 'File size must be less than 50MB';\n  }\n\n  return null;\n}\n\nexport function extractImageUrl(response: EditResponse): string | null {\n  return response.images?.[0]?.url || response.image?.url || null;\n}\n\nexport async function dataUrlToBlob(dataUrl: string): Promise<Blob> {\n  const response = await fetch(dataUrl);\n  return response.blob();\n}\n\nexport function downloadImage(imageUrl: string, filename: string = 'image.png') {\n  const link = document.createElement('a');\n  link.href = imageUrl;\n  link.download = filename;\n  document.body.appendChild(link);\n  link.click();\n  document.body.removeChild(link);\n}\n\nexport function getTimestamp(): string {\n  return new Date().toISOString().replace(/[:.]/g, '-');\n}\n\n"
  },
  {
    "path": "frontend/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n\n"
  },
  {
    "path": "frontend/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n\n"
  },
  {
    "path": "frontend/vite.config.ts",
    "content": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\n\nexport default defineConfig({\n  plugins: [react()],\n  server: {\n    port: 3000,\n    proxy: {\n      '/api': {\n        target: 'http://localhost:3001',\n        changeOrigin: true,\n      },\n    },\n  },\n});\n\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"nanobanana-studio\",\n  \"version\": \"1.0.0\",\n  \"description\": \"AI-powered image editor using Google's nanobanana API from fal.ai\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"concurrently \\\"npm run dev:frontend\\\" \\\"npm run dev:backend\\\"\",\n    \"dev:frontend\": \"cd frontend && npm run dev\",\n    \"dev:backend\": \"cd backend && npm run dev\",\n    \"build\": \"cd frontend && npm run build\",\n    \"install:all\": \"npm install && cd frontend && npm install && cd ../backend && npm install\"\n  },\n  \"keywords\": [\n    \"image-editor\",\n    \"ai\",\n    \"fal.ai\",\n    \"nanobanana\"\n  ],\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"concurrently\": \"^8.2.2\"\n  },\n  \"dependencies\": {\n    \"@fal-ai/client\": \"^1.7.2\"\n  }\n}\n"
  }
]