frontend changes
This commit is contained in:
parent
68bec83ba4
commit
99107c06c5
@ -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**
|
||||
@ -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
129
package-lock.json
generated
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
116
src/app/api/ai/tech-recommendations/route.ts
Normal file
116
src/app/api/ai/tech-recommendations/route.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -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');
|
||||
|
||||
332
src/components/business-context/typeform-survey.tsx
Normal file
332
src/components/business-context/typeform-survey.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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
@ -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}
|
||||
|
||||
42
src/components/ui/error-banner.tsx
Normal file
42
src/components/ui/error-banner.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
430
src/services/claudeTechRecommendations.ts
Normal file
430
src/services/claudeTechRecommendations.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user