frontend changes

This commit is contained in:
Chandini 2025-10-06 15:06:29 +05:30
parent 68bec83ba4
commit 99107c06c5
14 changed files with 2162 additions and 548 deletions

View File

@ -1,231 +0,0 @@
# Authentication Implementation
## Overview
This document describes the authentication system implementation with proper error handling, separate signup/signin routes, and email verification flow.
## Architecture
### Backend (User Auth Service)
- **Port**: 8011
- **Base URL**: `http://localhost:8011`
- **Database**: PostgreSQL
- **Features**: JWT authentication, email verification, session management
### Frontend (Next.js App)
- **Port**: 3001
- **Base URL**: `http://localhost:3001`
- **Framework**: Next.js 15.4.6 with TypeScript
- **UI**: Tailwind CSS + shadcn/ui components
## Routes Structure
### Frontend Routes
```
/signup - User registration page
/signin - User login page
/auth - Redirects to /signin (legacy support)
/verify-email - Email verification page (handled by backend redirect)
```
### Backend API Endpoints
```
POST /api/auth/register - User registration
POST /api/auth/login - User login
GET /api/auth/verify-email - Email verification (redirects to frontend)
POST /api/auth/logout - User logout
POST /api/auth/refresh - Token refresh
GET /api/auth/me - Get user profile
```
## User Flow
### 1. Registration Flow
1. User visits `/signup`
2. Fills out registration form
3. Submits form → `POST /api/auth/register`
4. Backend creates user account and sends verification email
5. Frontend shows success message and redirects to `/signin` after 3 seconds
6. User receives email with verification link
### 2. Email Verification Flow
1. User clicks verification link in email
2. Link points to: `http://localhost:8011/api/auth/verify-email?token=<token>`
3. Backend verifies token and redirects to: `http://localhost:3001/signin?verified=true`
4. Frontend displays success message: "Email verified successfully! You can now sign in to your account."
### 3. Login Flow
1. User visits `/signin`
2. Fills out login form
3. Submits form → `POST /api/auth/login`
4. Backend validates credentials and returns JWT tokens
5. Frontend stores tokens and redirects to dashboard (`/`)
## Error Handling
### Backend Error Responses
All API endpoints return consistent error format:
```json
{
"success": false,
"error": "Error type",
"message": "Detailed error message"
}
```
### Frontend Error Handling
- **Network Errors**: Display user-friendly messages
- **API Errors**: Show specific error messages from backend
- **Validation Errors**: Client-side validation with immediate feedback
- **Authentication Errors**: Clear messaging for login/registration issues
### Common Error Scenarios
#### Registration Errors
- **Email already exists**: "An account with this email already exists"
- **Username taken**: "Username is already taken"
- **Invalid email format**: "Please enter a valid email address"
- **Weak password**: "Password must be at least 8 characters long"
- **Missing fields**: "Please fill in all required fields"
#### Login Errors
- **Invalid credentials**: "Invalid email or password"
- **Email not verified**: "Please verify your email before signing in"
- **Account locked**: "Account is temporarily locked due to multiple failed attempts"
#### Email Verification Errors
- **Invalid token**: "Verification link is invalid or has expired"
- **Already verified**: "Email is already verified"
- **Token expired**: "Verification link has expired. Please request a new one"
## Components Structure
### Signup Flow
```
/signup
├── SignUpPage (main container)
├── SignUpForm (form component)
└── Success State (after registration)
```
### Signin Flow
```
/signin
├── SignInPage (main container)
├── SignInForm (form component)
└── Verification Messages (from URL params)
```
## API Integration
### Authentication Handler (`authenticationHandler.tsx`)
- Handles API calls to backend
- Proper error propagation
- TypeScript interfaces for type safety
### Key Functions
```typescript
registerUser(data: RegisterData): Promise<ApiResponse>
loginUser(email: string, password: string): Promise<ApiResponse>
```
## Security Features
### Backend Security
- JWT token authentication
- Password hashing (bcrypt)
- Rate limiting on auth endpoints
- CORS configuration
- Helmet security headers
- Session management
### Frontend Security
- Token storage in localStorage
- Automatic token refresh
- Secure API communication
- Input validation and sanitization
## Environment Configuration
### Backend (.env)
```env
PORT=8011
FRONTEND_URL=http://localhost:3001
POSTGRES_HOST=localhost
POSTGRES_DB=user_auth
JWT_SECRET=your-secret-key
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
```
### Frontend (.env.local)
```env
NEXT_PUBLIC_API_URL=http://localhost:8011
NEXT_PUBLIC_FRONTEND_URL=http://localhost:3001
```
## Testing the Implementation
### 1. Start Services
```bash
# Backend
cd automated-dev-pipeline
docker-compose up user-auth
# Frontend
cd codenuk-frontend-dark-theme
npm run dev
```
### 2. Test Registration
1. Visit `http://localhost:3001/signup`
2. Fill out registration form
3. Submit and check for success message
4. Check email for verification link
### 3. Test Email Verification
1. Click verification link in email
2. Should redirect to `http://localhost:3001/signin?verified=true`
3. Verify success message appears
### 4. Test Login
1. Visit `http://localhost:3001/signin`
2. Enter credentials
3. Should redirect to dashboard on success
## Troubleshooting
### Common Issues
1. **CORS Errors**
- Check backend CORS configuration
- Verify frontend URL in allowed origins
2. **Email Not Sending**
- Check SMTP configuration in backend
- Verify email credentials
3. **Verification Link Not Working**
- Check frontend URL in backend configuration
- Verify token expiration settings
4. **Login Fails After Verification**
- Check if user is properly verified in database
- Verify JWT token generation
### Debug Steps
1. Check browser network tab for API calls
2. Check backend logs for errors
3. Verify database connections
4. Test API endpoints directly with Postman/curl
## Future Enhancements
1. **Password Reset Flow**
2. **Two-Factor Authentication**
3. **Social Login Integration**
4. **Account Lockout Protection**
5. **Session Management Dashboard**
6. **Audit Logging**

View File

@ -1,118 +0,0 @@
# Dynamic Templates Implementation
## Overview
The frontend now fetches templates dynamically from the database instead of using static templates. This allows for real-time template management and custom template creation.
## Changes Made
### 1. Template Service (`src/lib/template-service.ts`)
- Created service to communicate with the template-manager API
- Handles fetching templates by category, individual templates, and creating new templates
- Includes TypeScript interfaces for type safety
### 2. Custom Hook (`src/hooks/useTemplates.ts`)
- Manages template data fetching and state
- Converts database templates to UI format
- Handles loading states and error handling
- Provides template feature fetching
### 3. Custom Template Form (`src/components/custom-template-form.tsx`)
- Form component for creating new templates
- Includes all required fields: type, title, description, category
- Optional fields: icon, gradient, border, text, subtext
- Validates required fields before submission
### 4. Updated Main Dashboard (`src/components/main-dashboard.tsx`)
- Replaced static templates with dynamic database templates
- Added loading and error states
- Dynamic category generation based on available templates
- Custom template creation functionality
- Fallback to static templates if database is unavailable
### 5. UI Components
- Added `textarea.tsx` component for form inputs
- Enhanced existing components with proper styling
## API Integration
### Template Manager Service
- **Base URL**: `http://localhost:8009`
- **Endpoints**:
- `GET /api/templates` - Get all templates grouped by category
- `GET /api/templates/:id` - Get specific template with features
- `GET /api/templates/type/:type` - Get template by type
- `POST /api/templates` - Create new template
### Database Schema
Templates are stored in PostgreSQL with the following structure:
```sql
CREATE TABLE templates (
id UUID PRIMARY KEY,
type VARCHAR(100) UNIQUE,
title VARCHAR(200),
description TEXT,
category VARCHAR(100),
icon VARCHAR(50),
gradient VARCHAR(100),
border VARCHAR(100),
text VARCHAR(100),
subtext VARCHAR(100),
is_active BOOLEAN,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
## Usage
### Viewing Templates
1. Templates are automatically loaded from the database on page load
2. If database is unavailable, fallback static templates are shown
3. Templates are grouped by category dynamically
### Creating Custom Templates
1. Click "Create Custom Template" button
2. Fill in required fields (type, title, description, category)
3. Optionally add styling fields (icon, gradient, border, text, subtext)
4. Submit to create the template in the database
5. Template will appear in the list after creation
### Template Features
- Each template can have associated features stored in `template_features` table
- Features are fetched when a template is selected
- Features include complexity, type, and usage statistics
## Error Handling
- Network errors show retry button
- Loading states with spinner
- Graceful fallback to static templates
- Form validation for required fields
## ✅ Implemented Features
### Template Management
- ✅ **Dynamic Template Display** - Templates fetched from database
- ✅ **Custom Template Creation** - Create new templates via form
- ✅ **Template Editing** - Edit existing templates
- ✅ **Template Deletion** - Delete templates with confirmation
- ✅ **Real-time Updates** - Changes reflect immediately in UI
### API Endpoints
- ✅ `GET /api/templates` - Get all templates grouped by category
- ✅ `GET /api/templates/:id` - Get specific template with features
- ✅ `POST /api/templates` - Create new template
- ✅ `PUT /api/templates/:id` - Update existing template
- ✅ `DELETE /api/templates/:id` - Delete template (soft delete)
### Database Operations
- ✅ **Soft Delete** - Templates are marked as inactive rather than physically deleted
- ✅ **Data Integrity** - All operations maintain referential integrity
- ✅ **Error Handling** - Comprehensive error handling for all operations
## Future Enhancements
- Feature management for templates
- Bulk template operations
- Template versioning
- Template sharing between users
- Template import/export functionality
- Template analytics and usage tracking

129
package-lock.json generated
View File

@ -34,8 +34,9 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.22",
"lucide-react": "^0.539.0",
"next": "15.4.6",
"next": "^15.5.4",
"react": "19.1.0",
"react-day-picker": "^9.9.0",
"react-dom": "19.1.0",
@ -932,9 +933,9 @@
}
},
"node_modules/@next/env": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.6.tgz",
"integrity": "sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.4.tgz",
"integrity": "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@ -948,9 +949,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz",
"integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz",
"integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==",
"cpu": [
"arm64"
],
@ -964,9 +965,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz",
"integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz",
"integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==",
"cpu": [
"x64"
],
@ -980,9 +981,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz",
"integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz",
"integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==",
"cpu": [
"arm64"
],
@ -996,9 +997,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz",
"integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz",
"integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==",
"cpu": [
"arm64"
],
@ -1012,9 +1013,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz",
"integrity": "sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.4.tgz",
"integrity": "sha512-7HKolaj+481FSW/5lL0BcTkA4Ueam9SPYWyN/ib/WGAFZf0DGAN8frNpNZYFHtM4ZstrHZS3LY3vrwlIQfsiMA==",
"cpu": [
"x64"
],
@ -1028,9 +1029,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.6.tgz",
"integrity": "sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz",
"integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==",
"cpu": [
"x64"
],
@ -1044,9 +1045,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz",
"integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz",
"integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==",
"cpu": [
"arm64"
],
@ -1060,9 +1061,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz",
"integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz",
"integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==",
"cpu": [
"x64"
],
@ -4605,9 +4606,9 @@
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@ -6124,6 +6125,33 @@
"integrity": "sha512-0tLU0FOedVY7lrvN4LK0DVj6FTuYM0pWDpN97/8UTZE2lx1+OwX8+2uL7IOWc2PmktYTHQjMT6FvZZ3SGCdZdg==",
"license": "CC0-1.0"
},
"node_modules/framer-motion": {
"version": "12.23.22",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz",
"integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.21",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -7542,6 +7570,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/motion-dom": {
"version": "12.23.21",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz",
"integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -7590,12 +7633,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "15.4.6",
"resolved": "https://registry.npmjs.org/next/-/next-15.4.6.tgz",
"integrity": "sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==",
"version": "15.5.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.4.tgz",
"integrity": "sha512-xH4Yjhb82sFYQfY3vbkJfgSDgXvBB6a8xPs9i35k6oZJRoQRihZH+4s9Yo2qsWpzBmZ3lPXaJ2KPXLfkvW4LnA==",
"license": "MIT",
"dependencies": {
"@next/env": "15.4.6",
"@next/env": "15.5.4",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@ -7608,14 +7651,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.4.6",
"@next/swc-darwin-x64": "15.4.6",
"@next/swc-linux-arm64-gnu": "15.4.6",
"@next/swc-linux-arm64-musl": "15.4.6",
"@next/swc-linux-x64-gnu": "15.4.6",
"@next/swc-linux-x64-musl": "15.4.6",
"@next/swc-win32-arm64-msvc": "15.4.6",
"@next/swc-win32-x64-msvc": "15.4.6",
"@next/swc-darwin-arm64": "15.5.4",
"@next/swc-darwin-x64": "15.5.4",
"@next/swc-linux-arm64-gnu": "15.5.4",
"@next/swc-linux-arm64-musl": "15.5.4",
"@next/swc-linux-x64-gnu": "15.5.4",
"@next/swc-linux-x64-musl": "15.5.4",
"@next/swc-win32-arm64-msvc": "15.5.4",
"@next/swc-win32-x64-msvc": "15.5.4",
"sharp": "^0.34.3"
},
"peerDependencies": {

View File

@ -35,8 +35,9 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.22",
"lucide-react": "^0.539.0",
"next": "15.4.6",
"next": "^15.5.4",
"react": "19.1.0",
"react-day-picker": "^9.9.0",
"react-dom": "19.1.0",

View File

@ -0,0 +1,116 @@
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
console.log('🚀 Tech recommendations API called - redirecting to unified service');
// Parse request body
const body = await request.json();
console.log('📊 Request body:', {
template: body.template?.title,
featuresCount: body.features?.length,
businessQuestionsCount: body.businessContext?.questions?.length,
});
// Validate required fields
if (!body.template || !body.features || !body.businessContext) {
return NextResponse.json(
{
success: false,
error: 'Missing required fields: template, features, or businessContext',
},
{ status: 400 }
);
}
// Validate template structure
if (!body.template.title || !body.template.category) {
return NextResponse.json(
{
success: false,
error: 'Template must have title and category',
},
{ status: 400 }
);
}
// Validate features array
if (!Array.isArray(body.features) || body.features.length === 0) {
return NextResponse.json(
{
success: false,
error: 'Features must be a non-empty array',
},
{ status: 400 }
);
}
// Validate business context
if (!body.businessContext.questions || !Array.isArray(body.businessContext.questions)) {
return NextResponse.json(
{
success: false,
error: 'Business context must have questions array',
},
{ status: 400 }
);
}
// Redirect to unified service through API Gateway
const apiGatewayUrl = process.env.BACKEND_URL || 'https://backend.codenuk.com';
const response = await fetch(`${apiGatewayUrl}/api/unified/comprehensive-recommendations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...body,
// Add optional parameters for template-based and domain-based recommendations
templateId: body.template.id,
budget: 15000, // Default budget - could be made configurable
domain: body.template.category?.toLowerCase() || 'general',
includeClaude: true,
includeTemplateBased: true,
includeDomainBased: true,
}),
});
if (!response.ok) {
throw new Error(`Unified service error: ${response.status}`);
}
const recommendations = await response.json();
console.log('✅ Comprehensive recommendations received from unified service:', {
success: recommendations.success,
hasClaudeRecommendations: !!recommendations.data?.claude?.success,
hasTemplateRecommendations: !!recommendations.data?.templateBased?.success,
hasDomainRecommendations: !!recommendations.data?.domainBased?.success,
});
// Return the recommendations
return NextResponse.json(recommendations);
} catch (error) {
console.error('❌ Error in tech recommendations API:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
},
{ status: 500 }
);
}
}
// Handle OPTIONS request for CORS
export async function OPTIONS(request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}

View File

@ -87,6 +87,7 @@ export const logout = async () => {
export const authApiClient = axios.create({
baseURL: BACKEND_URL,
withCredentials: true,
timeout: 10000, // 10 second timeout
});
// Add auth token to requests
@ -127,8 +128,34 @@ const addTokenRefreshInterceptor = (client: typeof authApiClient) => {
try {
const status = error?.response?.status
const data = error?.response?.data
console.error('🛑 API error:', { url: error?.config?.url, method: error?.config?.method, status, data })
} catch (_) {}
const url = error?.config?.url
const method = error?.config?.method
const message = error?.message || 'Unknown error'
const code = error?.code
// Check if it's a network connectivity issue
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT') {
console.error('🛑 Network connectivity issue:', {
url: url || 'Unknown URL',
method: method || 'Unknown method',
code: code,
message: message
})
} else {
console.error('🛑 API error:', {
url: url || 'Unknown URL',
method: method || 'Unknown method',
status: status || 'No status',
data: data || 'No response data',
message: message,
errorType: error?.name || 'Unknown error type',
code: code
})
}
} catch (debugError) {
console.error('🛑 Error logging failed:', debugError)
console.error('🛑 Original error:', error)
}
const originalRequest = error.config;
const isRefreshEndpoint = originalRequest?.url?.includes('/api/auth/refresh');

View File

@ -0,0 +1,332 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react'
interface Question {
id: number
question: string
answer: string
}
interface TypeformSurveyProps {
questions: string[]
onComplete: (answers: Question[]) => void
onProgress?: (answers: Question[]) => void
onBack: () => void
projectName?: string
}
export default function TypeformSurvey({
questions,
onComplete,
onProgress,
onBack,
projectName = 'your project'
}: TypeformSurveyProps) {
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0)
const [answers, setAnswers] = useState<Record<number, string>>({})
const [isTransitioning, setIsTransitioning] = useState(false)
const totalQuestions = questions.length
const progress = ((currentQuestionIndex + 1) / totalQuestions) * 100
const answeredCount = Object.values(answers).filter(answer => answer.trim()).length
// Initialize answers
useEffect(() => {
const initialAnswers: Record<number, string> = {}
questions.forEach((_, index) => {
initialAnswers[index] = ''
})
setAnswers(initialAnswers)
}, [questions])
const handleAnswerChange = (value: string) => {
const newAnswers = {
...answers,
[currentQuestionIndex]: value
}
setAnswers(newAnswers)
// Call onProgress callback if provided
if (onProgress) {
const questionAnswers: Question[] = questions.map((question, index) => ({
id: index,
question,
answer: newAnswers[index] || ''
}))
onProgress(questionAnswers)
}
}
const goToNext = useCallback(() => {
if (currentQuestionIndex < totalQuestions - 1) {
setIsTransitioning(true)
setTimeout(() => {
setCurrentQuestionIndex(prev => prev + 1)
setIsTransitioning(false)
}, 150)
} else {
// Submit all answers
const questionAnswers: Question[] = questions.map((question, index) => ({
id: index,
question,
answer: answers[index] || ''
}))
onComplete(questionAnswers)
}
}, [currentQuestionIndex, totalQuestions, answers, questions, onComplete])
const goToPrevious = useCallback(() => {
if (currentQuestionIndex > 0) {
setIsTransitioning(true)
setTimeout(() => {
setCurrentQuestionIndex(prev => prev - 1)
setIsTransitioning(false)
}, 150)
} else {
onBack()
}
}, [currentQuestionIndex, onBack])
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
goToNext()
}
}
const currentAnswer = answers[currentQuestionIndex] || ''
const canProceed = currentAnswer.trim().length > 0
return (
<div className="min-h-screen bg-black text-white relative overflow-hidden">
{/* Progress Bar */}
<div className="bg-black/80 backdrop-blur-sm border-b border-white/10">
<div className="max-w-4xl mx-auto px-6 py-4">
<div className="flex items-center justify-between mb-3">
<div className="text-sm text-white/60">
Question {currentQuestionIndex + 1} of {totalQuestions}
</div>
<div className="text-sm text-white/60">
{Math.round(progress)}% complete
</div>
</div>
<div className="w-full bg-white/10 rounded-full h-2">
<motion.div
className="bg-orange-500 h-2 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.3, ease: "easeOut" }}
/>
</div>
</div>
</div>
{/* Main Content */}
<div className="pt-4 pb-16 px-6">
<div className="max-w-4xl mx-auto">
<AnimatePresence mode="wait">
<motion.div
key={currentQuestionIndex}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="flex flex-col pt-8"
>
{/* Question */}
<div className="text-center mb-6">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1, duration: 0.4 }}
className="text-xl font-semibold leading-tight mb-4"
>
{questions[currentQuestionIndex]}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.4 }}
className="text-base text-white/60"
>
Help us understand your {projectName} better
</motion.p>
</div>
{/* Answer Input */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.4 }}
className="w-full mx-auto mb-6"
>
<textarea
value={currentAnswer}
onChange={(e) => handleAnswerChange(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="Type your answer here..."
className="w-full bg-white/5 border border-white/20 rounded-lg p-3 text-white placeholder:text-white/40 focus:outline-none focus:ring-1 focus:ring-orange-500 focus:border-orange-500 resize-none h-24 text-base leading-relaxed"
autoFocus
/>
</motion.div>
{/* Navigation */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.4 }}
className="flex items-center justify-between mt-6 max-w-6xl mx-auto w-full"
>
{/* Back Button */}
<button
onClick={goToPrevious}
className="flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 border border-white/20 rounded-lg transition-all duration-200 group"
>
<ChevronLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
<span>{currentQuestionIndex === 0 ? 'Back to Features' : 'Previous'}</span>
</button>
{/* Next Button */}
<button
onClick={goToNext}
disabled={!canProceed || isTransitioning}
className={`flex items-center gap-2 px-6 py-2 rounded-lg transition-all duration-200 group ${
canProceed
? 'bg-orange-500 hover:bg-orange-600 text-white'
: 'bg-white/10 text-white/40 cursor-not-allowed'
}`}
>
<span>{currentQuestionIndex === totalQuestions - 1 ? 'Submit' : 'Next'}</span>
{currentQuestionIndex === totalQuestions - 1 ? (
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
) : (
<ChevronRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
)}
</button>
</motion.div>
{/* Progress Indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.4 }}
className="text-center mt-4"
>
<div className="text-sm text-white/40">
{answeredCount} of {totalQuestions} questions answered
</div>
</motion.div>
</motion.div>
</AnimatePresence>
</div>
</div>
{/* Background Elements */}
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-orange-500/5 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-orange-500/3 rounded-full blur-3xl" />
</div>
</div>
)
}
// Summary Component for final review
interface SummaryProps {
questions: Question[]
onBack: () => void
onSubmit: () => void
loading?: boolean
}
export function SurveySummary({ questions, onBack, onSubmit, loading = false }: SummaryProps) {
const answeredQuestions = questions.filter(q => q.answer.trim())
return (
<div className="min-h-screen bg-black text-white">
{/* Header */}
<div className="border-b border-white/10 bg-black/80 backdrop-blur-sm">
<div className="max-w-4xl mx-auto px-6 py-4">
<h1 className="text-xl font-semibold text-center">Review Your Answers</h1>
<p className="text-center text-white/60 mt-1 text-sm">
Please review your responses before submitting
</p>
</div>
</div>
{/* Summary Content */}
<div className="max-w-4xl mx-auto px-6 py-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-4"
>
{answeredQuestions.map((question, index) => (
<motion.div
key={question.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="bg-white/5 border border-white/10 rounded-lg p-4"
>
<div className="flex items-start gap-3">
<div className="bg-orange-500/20 text-orange-300 rounded-full w-6 h-6 flex items-center justify-center text-xs font-semibold flex-shrink-0 mt-0.5">
{question.id + 1}
</div>
<div className="flex-1">
<h3 className="text-sm font-medium text-white mb-2">
{question.question}
</h3>
<p className="text-white/80 leading-relaxed text-sm">
{question.answer}
</p>
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Action Buttons */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="flex items-center justify-between mt-6"
>
<button
onClick={onBack}
className="flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 border border-white/20 rounded-lg transition-all duration-200"
>
<ChevronLeft className="w-4 h-4" />
<span>Edit Answers</span>
</button>
<button
onClick={onSubmit}
disabled={loading}
className={`flex items-center gap-2 px-6 py-2 rounded-lg transition-all duration-200 group ${
loading
? 'bg-orange-500/50 text-white cursor-not-allowed'
: 'bg-orange-500 hover:bg-orange-600 text-white'
}`}
>
{loading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>Generating Recommendations...</span>
</>
) : (
<>
<span>Submit & Generate Recommendations</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</>
)}
</button>
</motion.div>
</div>
</div>
)
}

View File

@ -18,12 +18,14 @@ import AICustomFeatureCreator from "@/components/ai/AICustomFeatureCreator"
import { BACKEND_URL } from "@/config/backend"
// Removed Tooltip import as we are no longer using tooltips for title/description
import WireframeCanvas from "@/components/wireframe-canvas"
import TypeformSurvey, { SurveySummary } from "@/components/business-context/typeform-survey"
import PromptSidePanel from "@/components/prompt-side-panel"
import { DualCanvasEditor } from "@/components/dual-canvas-editor"
import { getAccessToken } from "@/components/apis/authApiClients"
import TechStackSummary from "@/components/tech-stack-summary"
import { attachRepository, getGitHubAuthStatus } from "@/lib/api/github"
import ViewUserReposButton from "@/components/github/ViewUserReposButton"
import { ErrorBanner } from "@/components/ui/error-banner"
interface Template {
id: string
@ -70,13 +72,24 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
const [authUrl, setAuthUrl] = useState('')
const [isGeneratingAuth, setIsGeneratingAuth] = useState(false)
const [isGithubConnected, setIsGithubConnected] = useState<boolean | null>(null)
const [connectionError, setConnectionError] = useState<string | null>(null)
useEffect(() => {
(async () => {
try {
setConnectionError(null)
const status = await getGitHubAuthStatus()
setIsGithubConnected(!!status?.data?.connected)
} catch {
} catch (error: any) {
console.warn('Failed to check GitHub auth status:', error)
setIsGithubConnected(false)
// Check if it's a connectivity issue
if (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT') {
setConnectionError('Unable to connect to the server. Please ensure the backend is running.')
} else if (error?.response?.status >= 500) {
setConnectionError('Server error. Please try again later.')
}
}
})()
}, [])
@ -758,6 +771,30 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi
<p className="text-xl text-white/60 max-w-3xl mx-auto">
Select from our comprehensive library of professionally designed templates
</p>
{/* Connection Error Banner */}
{connectionError && (
<div className="max-w-2xl mx-auto">
<ErrorBanner
title="Connection Issue"
message={connectionError}
onRetry={async () => {
setConnectionError(null)
// Retry the GitHub auth status check
try {
const status = await getGitHubAuthStatus()
setIsGithubConnected(!!status?.data?.connected)
} catch (error: any) {
console.warn('Retry failed:', error)
setIsGithubConnected(false)
if (error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT') {
setConnectionError('Unable to connect to the server. Please ensure the backend is running.')
}
}
}}
/>
</div>
)}
{!user?.id && (
<div className="bg-orange-500/10 border border-orange-500/20 rounded-lg p-4 max-w-2xl mx-auto">
<p className="text-orange-300 text-sm">
@ -1699,10 +1736,12 @@ function BusinessQuestionsStep({
onDone,
}: { template: Template; selected: TemplateFeature[]; onBack: () => void; onDone: (completeData: any, recommendations: any) => void }) {
const [businessQuestions, setBusinessQuestions] = useState<string[]>([])
const [businessAnswers, setBusinessAnswers] = useState<Record<number, string>>({})
const [loading, setLoading] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [showSummary, setShowSummary] = useState(false)
const [answers, setAnswers] = useState<any[]>([])
const [submitting, setSubmitting] = useState(false)
const [answeredQuestionsCount, setAnsweredQuestionsCount] = useState(0)
const selectedKey = selected.map(s => s.id).join(',')
useEffect(() => {
@ -1729,9 +1768,6 @@ function BusinessQuestionsStep({
const data = await resp.json()
const qs: string[] = data?.data?.businessQuestions || []
setBusinessQuestions(qs)
const init: Record<number, string> = {}
qs.forEach((_, i) => (init[i] = ''))
setBusinessAnswers(init)
} catch (e: any) {
setError(e?.message || 'Failed to load questions')
} finally {
@ -1741,7 +1777,91 @@ function BusinessQuestionsStep({
load()
}, [template.id, selectedKey])
const answeredCount = Object.values(businessAnswers).filter((a) => a && a.trim()).length
const handleSurveyComplete = (surveyAnswers: any[]) => {
setAnswers(surveyAnswers)
setAnsweredQuestionsCount(surveyAnswers.length)
setShowSummary(true)
}
const handleSurveyProgress = (currentAnswers: any[]) => {
const answeredCount = currentAnswers.filter(answer => answer.answer && answer.answer.trim().length > 0).length
setAnsweredQuestionsCount(answeredCount)
// Store the current answers so they're available for submission
setAnswers(currentAnswers)
}
const handleSubmit = async () => {
try {
setSubmitting(true)
setError(null)
const token = getAccessToken()
// Filter to only include answered questions
const answeredQuestions = answers.filter(answer => answer.answer && answer.answer.trim().length > 0)
const questionsWithAnswers = answeredQuestions.map(answer => ({
question: answer.question,
answer: answer.answer,
}))
const completeData = {
template,
features: selected,
businessContext: {
questions: questionsWithAnswers,
},
projectName: template.title,
projectType: template.type || template.category,
}
console.log('🚀 Submitting comprehensive recommendations request...')
console.log('📊 Total answers received:', answers.length)
console.log('📊 Answered questions:', answeredQuestions.length)
console.log('📊 Questions with answers:', questionsWithAnswers)
console.log('📊 Complete data:', completeData)
const response = await fetch(`${BACKEND_URL}/api/unified/comprehensive-recommendations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({
template,
features: selected,
businessContext: {
questions: questionsWithAnswers,
},
projectName: template.title,
projectType: template.type || template.category,
templateId: template.id,
budget: 15000, // Default budget - could be made configurable
domain: template.category?.toLowerCase() || 'general',
includeClaude: true,
includeTemplateBased: true,
includeDomainBased: true,
}),
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const recommendations = await response.json()
console.log('✅ Comprehensive recommendations received:', recommendations)
// Extract the tech recommendations from the response for the frontend component
const techRecommendations = recommendations?.data?.claude?.data?.claude_recommendations || null
console.log('🎯 Extracted tech recommendations:', techRecommendations)
// Proceed to next step with complete data and recommendations
onDone(completeData, techRecommendations)
} catch (error: any) {
console.error('❌ Error submitting business context:', error)
setError(error?.message || 'Failed to submit business context')
} finally {
setSubmitting(false)
}
}
if (loading) {
return (
@ -1770,89 +1890,68 @@ function BusinessQuestionsStep({
)
}
const handleSubmit = async () => {
try {
setSubmitting(true)
const answeredCount = Object.values(businessAnswers).filter((a) => a && a.trim()).length
if (answeredCount === 0) return
const completeData = {
projectName: template.title,
projectType: template.type || template.category,
allFeatures: selected,
businessQuestions,
businessAnswers,
timestamp: new Date().toISOString(),
featureName: `${template.title} - Integrated System`,
description: `Complete ${template.type || template.category} system with ${selected.length} integrated features`,
requirements: (selected as any[]).flatMap((f: any) => f.requirements || []),
complexity:
(selected as any[]).some((f: any) => f.complexity === 'high')
? 'high'
: (selected as any[]).some((f: any) => f.complexity === 'medium')
? 'medium'
: 'low',
logicRules: (selected as any[]).flatMap((f: any) => f.logicRules || []),
}
// TODO: Store business context Q&A responses in backend (temporarily disabled)
console.log('📝 Business context answers:', businessAnswers)
// Proceed directly to next step with complete data
onDone(completeData, null)
} catch (e) {
console.error('Tech stack selection failed', e)
} finally {
setSubmitting(false)
}
if (showSummary) {
return (
<SurveySummary
questions={answers}
onBack={() => setShowSummary(false)}
onSubmit={handleSubmit}
loading={submitting}
/>
)
}
return (
<div className="space-y-6">
<div className="text-center space-y-1">
<h2 className="text-2xl font-bold text-white">Business Context Questions</h2>
<p className="text-white/60">Help us refine recommendations by answering these questions.</p>
<p className="text-sm text-orange-400">Analyzing {selected.length} integrated features</p>
<div className="max-w-6xl mx-auto space-y-4">
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold text-white">Business Context Questions</h1>
<p className="text-lg text-white/60 max-w-2xl mx-auto">
Help us understand your {template.title} project better with these AI-generated questions
</p>
</div>
<div className="space-y-4">
{businessQuestions.map((q, i) => (
<div key={i} className="bg-white/5 border border-white/10 rounded-lg p-4">
<label className="block text-sm font-medium text-white mb-2">
<span className="inline-flex items-center gap-2">
<span className="bg-orange-500/20 text-orange-300 rounded-full w-6 h-6 flex items-center justify-center text-xs font-semibold">{i + 1}</span>
<span>{q}</span>
</span>
</label>
<textarea
rows={3}
value={businessAnswers[i] || ''}
onChange={(e) => setBusinessAnswers((prev) => ({ ...prev, [i]: e.target.value }))}
className="w-full bg-white/10 border border-white/20 text-white rounded-md p-3 placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-orange-400/50"
placeholder="Your answer..."
/>
</div>
))}
</div>
<TypeformSurvey
questions={businessQuestions}
onComplete={handleSurveyComplete}
onProgress={handleSurveyProgress}
onBack={onBack}
projectName={template.title}
/>
<div className="bg-white/5 border border-white/10 rounded-lg p-4 text-white/80">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>Questions answered: {answeredCount} of {businessQuestions.length}</div>
<div>Completion: {businessQuestions.length ? Math.round((answeredCount / businessQuestions.length) * 100) : 0}%</div>
<div>Features analyzing: {selected.length}</div>
</div>
</div>
<div className="text-center pt-2">
{/* Navigation buttons */}
<div className="text-center py-4">
<div className="space-x-4">
<Button variant="outline" onClick={onBack} className="border-white/20 text-white hover:bg-white/10">Back</Button>
<Button
disabled={submitting || answeredCount === 0}
onClick={handleSubmit}
className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow ${submitting || answeredCount === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
<Button
variant="outline"
onClick={onBack}
className="border-white/20 text-white hover:bg-white/10 cursor-pointer"
>
{submitting ? 'Getting Recommendations...' : 'Generate Technology Recommendations'}
Back to Features
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || answeredQuestionsCount < 3}
className={`bg-orange-500 hover:bg-orange-400 text-black font-semibold py-2 rounded-lg shadow cursor-pointer ${
submitting || answeredQuestionsCount < 3 ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{submitting ? (
<div className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-black"></div>
Generating Tech Stack...
</div>
) : (
<>
Generate Tech Stack
</>
)}
</Button>
</div>
<div className="text-white/60 text-sm mt-2">
{answeredQuestionsCount < 3
? `Answer at least 3 questions to generate your tech stack recommendations (${answeredQuestionsCount}/3 answered)`
: `Ready to generate tech stack for ${template.title} (${answeredQuestionsCount} questions answered)`
}
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -121,7 +121,7 @@ function DialogDescription({
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
<div
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}

View File

@ -0,0 +1,42 @@
"use client"
import { AlertCircle, RefreshCw } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
interface ErrorBannerProps {
title?: string
message?: string
onRetry?: () => void
showRetry?: boolean
}
export function ErrorBanner({
title = "Connection Issue",
message = "Unable to connect to the server. Please check your internet connection and try again.",
onRetry,
showRetry = true
}: ErrorBannerProps) {
return (
<Card className="border-red-200 bg-red-50">
<CardContent className="flex items-center gap-3 p-4">
<AlertCircle className="h-5 w-5 text-red-600" />
<div className="flex-1">
<h3 className="font-medium text-red-800">{title}</h3>
<p className="text-sm text-red-700">{message}</p>
</div>
{showRetry && onRetry && (
<Button
variant="outline"
size="sm"
onClick={onRetry}
className="border-red-300 text-red-700 hover:bg-red-100"
>
<RefreshCw className="h-4 w-4 mr-2" />
Retry
</Button>
)}
</CardContent>
</Card>
)
}

View File

@ -1,8 +1,6 @@
//
// export const BACKEND_URL = 'https://backend.codenuk.com';
export const BACKEND_URL = 'http://localhost:8000';
export const BACKEND_URL = 'https://backend.codenuk.com';
export const SOCKET_URL = BACKEND_URL;

View File

@ -156,11 +156,24 @@ export interface GitHubAuthStatusData {
}
export async function getGitHubAuthStatus(): Promise<AttachRepositoryResponse<GitHubAuthStatusData>> {
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null
const url = userId ? `/api/github/auth/github/status?user_id=${encodeURIComponent(userId)}` : '/api/github/auth/github/status'
const response = await authApiClient.get(url)
return response.data as AttachRepositoryResponse<GitHubAuthStatusData>
try {
const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null
const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null
const url = userId ? `/api/github/auth/github/status?user_id=${encodeURIComponent(userId)}` : '/api/github/auth/github/status'
const response = await authApiClient.get(url)
return response.data as AttachRepositoryResponse<GitHubAuthStatusData>
} catch (error: any) {
console.error('GitHub auth status check failed:', error)
// Return a default response indicating no connection
return {
success: false,
message: 'Failed to check GitHub auth status',
data: {
connected: false,
requires_auth: true
}
}
}
}

View File

@ -0,0 +1,430 @@
import Anthropic from '@anthropic-ai/sdk';
// Initialize Claude API client
const anthropic = new Anthropic({
apiKey: process.env.NEXT_PUBLIC_CLAUDE_API_KEY || '',
});
export interface TemplateData {
id: string;
title: string;
description: string;
category: string;
type?: string;
}
export interface FeatureData {
id: string;
name: string;
description: string;
feature_type: 'essential' | 'suggested' | 'custom';
complexity: 'low' | 'medium' | 'high';
business_rules?: any;
technical_requirements?: any;
}
export interface BusinessContextData {
questions: Array<{
question: string;
answer: string;
}>;
}
export interface TechRecommendationRequest {
template: TemplateData;
features: FeatureData[];
businessContext: BusinessContextData;
projectName?: string;
projectType?: string;
}
export interface TechnologyRecommendations {
frontend: {
framework: string;
libraries: string[];
reasoning: string;
};
backend: {
language: string;
framework: string;
libraries: string[];
reasoning: string;
};
database: {
primary: string;
secondary: string[];
reasoning: string;
};
mobile: {
framework: string;
libraries: string[];
reasoning: string;
};
devops: {
tools: string[];
platforms: string[];
reasoning: string;
};
tools: {
development: string[];
monitoring: string[];
reasoning: string;
};
ai_ml: {
frameworks: string[];
libraries: string[];
reasoning: string;
};
}
export interface ClaudeRecommendations {
technology_recommendations: TechnologyRecommendations;
implementation_strategy: {
architecture_pattern: string;
deployment_strategy: string;
scalability_approach: string;
};
business_alignment: {
scalability: string;
maintainability: string;
cost_effectiveness: string;
time_to_market: string;
};
risk_assessment: {
technical_risks: string[];
mitigation_strategies: string[];
};
}
export interface TechRecommendationResponse {
success: boolean;
data: {
claude_recommendations: ClaudeRecommendations;
functional_requirements: {
feature_name: string;
description: string;
complexity_level: string;
technical_requirements: string[];
business_logic_rules: string[];
all_features: string[];
};
};
error?: string;
}
/**
* Generate technology recommendations using Claude AI
*/
export async function generateTechRecommendations(
request: TechRecommendationRequest
): Promise<TechRecommendationResponse> {
try {
console.log('🤖 Generating tech recommendations with Claude...');
console.log('📊 Request data:', {
template: request.template.title,
featuresCount: request.features.length,
businessQuestionsCount: request.businessContext.questions.length,
});
// Build comprehensive prompt for Claude
const prompt = buildTechRecommendationPrompt(request);
// Call Claude API
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 4000,
temperature: 0.7,
messages: [
{
role: 'user',
content: prompt,
},
],
});
// Parse Claude's response
const claudeResponse = response.content[0];
if (claudeResponse.type !== 'text') {
throw new Error('Unexpected response type from Claude');
}
const recommendations = parseClaudeResponse(claudeResponse.text, request);
console.log('✅ Tech recommendations generated successfully');
return {
success: true,
data: recommendations,
};
} catch (error) {
console.error('❌ Error generating tech recommendations:', error);
return {
success: false,
data: {
claude_recommendations: getFallbackRecommendations(),
functional_requirements: getFallbackFunctionalRequirements(request),
},
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Build comprehensive prompt for Claude AI
*/
function buildTechRecommendationPrompt(request: TechRecommendationRequest): string {
const { template, features, businessContext, projectName, projectType } = request;
// Extract feature information
const featureNames = features.map(f => f.name).join(', ');
const featureDescriptions = features.map(f => `- ${f.name}: ${f.description}`).join('\n');
const complexityLevels = features.map(f => f.complexity);
const hasHighComplexity = complexityLevels.includes('high');
const hasMediumComplexity = complexityLevels.includes('medium');
// Extract business context
const businessAnswers = businessContext.questions
.map(qa => `Q: ${qa.question}\nA: ${qa.answer}`)
.join('\n\n');
return `You are an expert technology architect and consultant. Analyze the following project requirements and provide comprehensive technology recommendations.
PROJECT OVERVIEW:
- Project Name: ${projectName || template.title}
- Project Type: ${projectType || template.category}
- Template: ${template.title} - ${template.description}
SELECTED FEATURES (${features.length} total):
${featureDescriptions}
COMPLEXITY ANALYSIS:
- High complexity features: ${hasHighComplexity ? 'Yes' : 'No'}
- Medium complexity features: ${hasMediumComplexity ? 'Yes' : 'No'}
- Overall complexity: ${hasHighComplexity ? 'High' : hasMediumComplexity ? 'Medium' : 'Low'}
BUSINESS CONTEXT:
${businessAnswers}
Please provide a comprehensive technology recommendation in the following JSON format:
{
"technology_recommendations": {
"frontend": {
"framework": "Recommended frontend framework",
"libraries": ["library1", "library2"],
"reasoning": "Detailed explanation for frontend choice"
},
"backend": {
"language": "Recommended backend language",
"framework": "Recommended backend framework",
"libraries": ["library1", "library2"],
"reasoning": "Detailed explanation for backend choice"
},
"database": {
"primary": "Primary database recommendation",
"secondary": ["secondary1", "secondary2"],
"reasoning": "Detailed explanation for database choice"
},
"mobile": {
"framework": "Recommended mobile framework (if applicable)",
"libraries": ["library1", "library2"],
"reasoning": "Detailed explanation for mobile choice"
},
"devops": {
"tools": ["tool1", "tool2"],
"platforms": ["platform1", "platform2"],
"reasoning": "Detailed explanation for DevOps choices"
},
"tools": {
"development": ["dev_tool1", "dev_tool2"],
"monitoring": ["monitoring_tool1", "monitoring_tool2"],
"reasoning": "Detailed explanation for tool choices"
},
"ai_ml": {
"frameworks": ["framework1", "framework2"],
"libraries": ["library1", "library2"],
"reasoning": "Detailed explanation for AI/ML choices"
}
},
"implementation_strategy": {
"architecture_pattern": "Recommended architecture pattern (e.g., MVC, Microservices, etc.)",
"deployment_strategy": "Recommended deployment approach",
"scalability_approach": "How to handle scaling"
},
"business_alignment": {
"scalability": "How the tech stack supports scalability",
"maintainability": "How the tech stack supports maintainability",
"cost_effectiveness": "Cost considerations and optimization",
"time_to_market": "How the tech stack affects development speed"
},
"risk_assessment": {
"technical_risks": ["risk1", "risk2"],
"mitigation_strategies": ["strategy1", "strategy2"]
}
}
CONSIDERATIONS:
1. Choose technologies that work well together
2. Consider the complexity level of features
3. Factor in business requirements from the context
4. Prioritize scalability and maintainability
5. Consider developer experience and community support
6. Balance performance with development speed
7. Include modern, actively maintained technologies
Provide only the JSON response, no additional text.`;
}
/**
* Parse Claude's response and structure it properly
*/
function parseClaudeResponse(responseText: string, request: TechRecommendationRequest): any {
try {
// Extract JSON from response (handle cases where Claude adds extra text)
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('No JSON found in Claude response');
}
const parsed = JSON.parse(jsonMatch[0]);
// Validate required fields
if (!parsed.technology_recommendations) {
throw new Error('Missing technology_recommendations in response');
}
// Build functional requirements from the request
const functionalRequirements = {
feature_name: `${request.template.title} - Integrated System`,
description: `Complete ${request.template.category} system with ${request.features.length} integrated features`,
complexity_level: request.features.some(f => f.complexity === 'high') ? 'high' :
request.features.some(f => f.complexity === 'medium') ? 'medium' : 'low',
technical_requirements: request.features.flatMap(f => f.technical_requirements || []),
business_logic_rules: request.features.flatMap(f => f.business_rules || []),
all_features: request.features.map(f => f.name),
};
return {
claude_recommendations: parsed,
functional_requirements: functionalRequirements,
};
} catch (error) {
console.error('Error parsing Claude response:', error);
throw new Error('Failed to parse Claude response');
}
}
/**
* Fallback recommendations when Claude API fails
*/
function getFallbackRecommendations(): ClaudeRecommendations {
return {
technology_recommendations: {
frontend: {
framework: 'React',
libraries: ['TypeScript', 'Tailwind CSS', 'React Router'],
reasoning: 'React provides excellent component reusability and ecosystem support for modern web applications.',
},
backend: {
language: 'Node.js',
framework: 'Express.js',
libraries: ['TypeScript', 'Prisma', 'JWT'],
reasoning: 'Node.js offers great performance and JavaScript ecosystem consistency between frontend and backend.',
},
database: {
primary: 'PostgreSQL',
secondary: ['Redis'],
reasoning: 'PostgreSQL provides robust ACID compliance and excellent performance for complex applications.',
},
mobile: {
framework: 'React Native',
libraries: ['Expo', 'React Navigation', 'AsyncStorage'],
reasoning: 'React Native enables cross-platform mobile development with shared codebase and native performance.',
},
devops: {
tools: ['Docker', 'GitHub Actions', 'Kubernetes'],
platforms: ['AWS', 'Vercel'],
reasoning: 'Modern DevOps stack for containerization, CI/CD, and cloud deployment with excellent scalability.',
},
tools: {
development: ['VS Code', 'Git', 'ESLint', 'Prettier'],
monitoring: ['Sentry', 'LogRocket', 'New Relic'],
reasoning: 'Essential development tools for code quality and comprehensive monitoring for production applications.',
},
ai_ml: {
frameworks: ['TensorFlow.js', 'OpenAI API'],
libraries: ['NumPy', 'Pandas', 'Scikit-learn'],
reasoning: 'AI/ML capabilities for data analysis, machine learning, and integration with modern AI services.',
},
},
implementation_strategy: {
architecture_pattern: 'MVC (Model-View-Controller)',
deployment_strategy: 'Containerized deployment with Docker',
scalability_approach: 'Horizontal scaling with load balancing',
},
business_alignment: {
scalability: 'Designed for horizontal scaling with microservices architecture',
maintainability: 'Modular architecture with clear separation of concerns',
cost_effectiveness: 'Open-source technologies reduce licensing costs',
time_to_market: 'Rapid development with modern frameworks and tools',
},
risk_assessment: {
technical_risks: ['Learning curve for new technologies', 'Integration complexity'],
mitigation_strategies: ['Comprehensive documentation', 'Phased implementation approach'],
},
};
}
/**
* Fallback functional requirements when Claude API fails
*/
function getFallbackFunctionalRequirements(request: TechRecommendationRequest): any {
return {
feature_name: `${request.template.title} - Integrated System`,
description: `Complete ${request.template.category} system with ${request.features.length} integrated features`,
complexity_level: request.features.some(f => f.complexity === 'high') ? 'high' :
request.features.some(f => f.complexity === 'medium') ? 'medium' : 'low',
technical_requirements: request.features.flatMap(f => f.technical_requirements || []),
business_logic_rules: request.features.flatMap(f => f.business_rules || []),
all_features: request.features.map(f => f.name),
};
}
/**
* Test function to validate Claude API integration
*/
export async function testClaudeIntegration(): Promise<boolean> {
try {
const testRequest: TechRecommendationRequest = {
template: {
id: 'test',
title: 'Test Project',
description: 'A test project for validation',
category: 'Web Application',
type: 'web-app',
},
features: [
{
id: 'test-feature',
name: 'User Authentication',
description: 'Basic user login and registration',
feature_type: 'essential',
complexity: 'medium',
},
],
businessContext: {
questions: [
{
question: 'What is your target audience?',
answer: 'Small to medium businesses',
},
],
},
};
const result = await generateTechRecommendations(testRequest);
return result.success;
} catch (error) {
console.error('Claude integration test failed:', error);
return false;
}
}