506 lines
20 KiB
Python
506 lines
20 KiB
Python
"""
|
|
REACT FRONTEND HANDLER - FIXED JSON VERSION
|
|
=====================
|
|
Expert-level React code generation with context preservation
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import asyncio
|
|
from datetime import datetime
|
|
from typing import Dict, Any, List, Optional
|
|
|
|
from src.handlers.base_handler import TechnologyHandler, HandlerResult, ContextChunk
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ReactHandler(TechnologyHandler):
|
|
"""Expert React frontend code generator"""
|
|
|
|
def __init__(self, contract_registry, event_bus, claude_client=None):
|
|
super().__init__(contract_registry, event_bus, claude_client)
|
|
self.handler_type = "react_frontend"
|
|
|
|
# React-specific configuration
|
|
self.react_patterns = {
|
|
"authentication": {
|
|
"components": ["LoginForm", "AuthProvider", "ProtectedRoute"],
|
|
"hooks": ["useAuth", "useAuthContext"],
|
|
"services": ["authService", "tokenManager"]
|
|
},
|
|
"user_management": {
|
|
"components": ["UserList", "UserForm", "UserProfile"],
|
|
"hooks": ["useUsers", "useUserForm"],
|
|
"services": ["userService"]
|
|
},
|
|
"real_time_chat": {
|
|
"components": ["ChatRoom", "MessageList", "MessageInput"],
|
|
"hooks": ["useSocket", "useMessages"],
|
|
"services": ["socketService", "messageService"]
|
|
}
|
|
}
|
|
|
|
# Quality validation patterns
|
|
self.quality_patterns = {
|
|
"error_handling": r"try\s*{|catch\s*\(|\.catch\(|error\s*&&",
|
|
"loading_states": r"loading|isLoading|pending",
|
|
"typescript_types": r"interface\s+\w+|type\s+\w+\s*=",
|
|
"proper_hooks": r"useEffect|useState|useCallback|useMemo",
|
|
"accessibility": r"aria-|role=|alt=",
|
|
"security": r"sanitize|escape|validate"
|
|
}
|
|
|
|
async def _generate_with_chunked_context(self, features: List[str],
|
|
context_chunks: List[ContextChunk],
|
|
correlation_id: str) -> HandlerResult:
|
|
"""Generate React code using chunked context"""
|
|
|
|
if not self.claude_client:
|
|
raise Exception("Claude client not initialized")
|
|
|
|
# Build expert React prompt
|
|
prompt = self._build_expert_prompt(features, context_chunks)
|
|
|
|
try:
|
|
# Make Claude API call with retry logic
|
|
response = await self._claude_request_with_retry(prompt, max_tokens=8000)
|
|
response_text = response.content[0].text
|
|
|
|
# Parse response into structured code
|
|
parsed_code = self._parse_react_response(response_text)
|
|
|
|
# Validate code quality
|
|
quality_report = await self._validate_code_quality(parsed_code)
|
|
|
|
# Extract contracts from generated code
|
|
contracts = self._extract_react_contracts(parsed_code, features)
|
|
|
|
return HandlerResult(
|
|
success=True,
|
|
handler_type=self.handler_type,
|
|
features_implemented=features,
|
|
code_files=parsed_code,
|
|
contracts=contracts,
|
|
quality_score=quality_report["overall_score"],
|
|
tokens_used=response.usage.input_tokens + response.usage.output_tokens if hasattr(response, 'usage') else 0
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ React generation failed: {e}")
|
|
raise e
|
|
|
|
def _build_expert_prompt(self, features: List[str], context_chunks: List[ContextChunk]) -> str:
|
|
"""Build expert-level React prompt with context"""
|
|
|
|
# Combine context chunks
|
|
context_content = "\n\n".join([
|
|
f"=== {chunk.chunk_type.upper()} ===\n{chunk.content}"
|
|
for chunk in context_chunks
|
|
])
|
|
|
|
# Get existing contracts
|
|
existing_contracts = ""
|
|
for feature in features:
|
|
contract = self.contracts.get_feature_contract(feature)
|
|
if contract:
|
|
endpoints = "\n".join([f" {ep.method} {ep.path}" for ep in contract.endpoints])
|
|
existing_contracts += f"\n{feature} API:\n{endpoints}\n"
|
|
|
|
features_text = "\n".join([f"- {feature.replace('_', ' ').title()}" for feature in features])
|
|
|
|
prompt = f"""You are an EXPERT React developer with 10+ years of enterprise experience. Generate PRODUCTION-READY React components with PERFECT code quality.
|
|
|
|
{context_content}
|
|
|
|
EXISTING API CONTRACTS TO INTEGRATE:
|
|
{existing_contracts}
|
|
|
|
FEATURES TO IMPLEMENT:
|
|
{features_text}
|
|
|
|
REACT REQUIREMENTS:
|
|
1. **TypeScript**: Use proper interfaces and types
|
|
2. **Modern Hooks**: useState, useEffect, useCallback, useMemo appropriately
|
|
3. **Error Handling**: Try/catch blocks, error boundaries, loading states
|
|
4. **Accessibility**: ARIA labels, semantic HTML, keyboard navigation
|
|
5. **Performance**: React.memo, useMemo for expensive calculations
|
|
6. **Security**: Input validation, XSS prevention, sanitization
|
|
7. **State Management**: Redux Toolkit with RTK Query for API calls
|
|
8. **Styling**: Styled-components or CSS modules
|
|
9. **Testing**: Component structure ready for Jest/RTL
|
|
|
|
ARCHITECTURE PATTERNS:
|
|
- Feature-based folder structure
|
|
- Custom hooks for business logic
|
|
- Service layer for API calls
|
|
- Context providers for global state
|
|
- Higher-order components for reusability
|
|
|
|
CRITICAL JSON RESPONSE REQUIREMENTS:
|
|
- Your response MUST be ONLY valid JSON. No explanations, no markdown, no code blocks.
|
|
- Start with {{ and end with }}. Nothing else.
|
|
- Do NOT use ```json or ``` anywhere in your response.
|
|
- Each file path maps to complete working code as a string.
|
|
- Use \\n for line breaks in code strings.
|
|
|
|
RESPONSE FORMAT - ONLY THIS JSON STRUCTURE:
|
|
{{"src/components/LoginForm.tsx": "import React, {{ useState }} from 'react';\\n\\nconst LoginForm = () => {{\\n const [email, setEmail] = useState('');\\n const [password, setPassword] = useState('');\\n // COMPLETE WORKING CODE HERE\\n}};\\n\\nexport default LoginForm;", "src/components/SignupForm.tsx": "import React, {{ useState }} from 'react';\\n\\nconst SignupForm = () => {{\\n const [formData, setFormData] = useState({{}});\\n // COMPLETE WORKING CODE HERE\\n}};\\n\\nexport default SignupForm;"}}
|
|
|
|
EXAMPLE CORRECT RESPONSE:
|
|
{{"file1.tsx": "const code = 'here';", "file2.ts": "export const api = 'code';"}}
|
|
|
|
EXAMPLE WRONG RESPONSE (DO NOT DO THIS):
|
|
```json
|
|
{{"file": "code"}}
|
|
```
|
|
|
|
CRITICAL REQUIREMENTS:
|
|
- COMPLETE, WORKING components (no placeholders)
|
|
- Proper TypeScript interfaces
|
|
- Comprehensive error handling
|
|
- Loading and error states
|
|
- Responsive design patterns
|
|
- Accessibility compliance
|
|
- Security best practices
|
|
- Integration with existing API contracts
|
|
|
|
Generate ONLY the JSON object. No other text. Implement ALL features with complete functionality."""
|
|
|
|
return prompt
|
|
|
|
def _parse_react_response(self, response: str) -> Dict[str, str]:
|
|
"""Parse Claude's React response into structured code files"""
|
|
|
|
try:
|
|
# Try direct JSON parsing first
|
|
response_clean = response.strip()
|
|
|
|
# Find JSON boundaries
|
|
start_idx = response_clean.find('{')
|
|
end_idx = response_clean.rfind('}') + 1
|
|
|
|
if start_idx != -1 and end_idx > start_idx:
|
|
json_content = response_clean[start_idx:end_idx]
|
|
parsed = json.loads(json_content)
|
|
|
|
# Validate structure
|
|
if isinstance(parsed, dict) and all(
|
|
isinstance(k, str) and isinstance(v, str)
|
|
for k, v in parsed.items()
|
|
):
|
|
return parsed
|
|
|
|
# Fallback: Extract code blocks
|
|
return self._extract_code_blocks_fallback(response)
|
|
|
|
except json.JSONDecodeError as e:
|
|
logger.warning(f"JSON parsing failed: {e}, using fallback extraction")
|
|
return self._extract_code_blocks_fallback(response)
|
|
|
|
def _extract_code_blocks_fallback(self, response: str) -> Dict[str, str]:
|
|
"""Fallback method to extract React code blocks"""
|
|
|
|
code_files = {}
|
|
|
|
# Pattern to match file paths and code blocks
|
|
file_pattern = r'(?:```(?:typescript|tsx|ts|javascript|jsx)?\s*)?(?://\s*)?([^\n]*\.(?:tsx?|jsx?|ts))\s*\n(.*?)(?=\n\s*(?://|```|\w+/)|$)'
|
|
|
|
matches = re.findall(file_pattern, response, re.DOTALL)
|
|
|
|
for file_path, code_content in matches:
|
|
file_path = file_path.strip().strip('"\'')
|
|
code_content = code_content.strip()
|
|
|
|
# Clean up code content
|
|
if code_content.startswith('```'):
|
|
code_content = '\n'.join(code_content.split('\n')[1:])
|
|
if code_content.endswith('```'):
|
|
code_content = '\n'.join(code_content.split('\n')[:-1])
|
|
|
|
if file_path and code_content and len(code_content) > 50:
|
|
code_files[file_path] = code_content
|
|
|
|
# If still no files found, create basic structure
|
|
if not code_files:
|
|
logger.warning("No code files extracted, creating basic structure")
|
|
code_files = {
|
|
"src/components/App.tsx": self._generate_basic_app_component(),
|
|
"src/index.tsx": self._generate_basic_index_file()
|
|
}
|
|
|
|
return code_files
|
|
|
|
async def _validate_code_quality(self, code_files: Dict[str, str]) -> Dict[str, Any]:
|
|
"""Validate React code quality with detailed scoring"""
|
|
|
|
total_score = 0
|
|
file_scores = {}
|
|
issues = []
|
|
|
|
for file_path, content in code_files.items():
|
|
file_score = self._validate_single_file_quality(file_path, content)
|
|
file_scores[file_path] = file_score
|
|
total_score += file_score["score"]
|
|
issues.extend(file_score["issues"])
|
|
|
|
overall_score = total_score / len(code_files) if code_files else 0
|
|
|
|
return {
|
|
"overall_score": overall_score,
|
|
"file_scores": file_scores,
|
|
"issues": issues,
|
|
"metrics": {
|
|
"total_files": len(code_files),
|
|
"average_score": overall_score,
|
|
"files_above_8": sum(1 for score in file_scores.values() if score["score"] >= 8.0),
|
|
"critical_issues": len([i for i in issues if i.startswith("CRITICAL")])
|
|
}
|
|
}
|
|
|
|
def _validate_single_file_quality(self, file_path: str, content: str) -> Dict[str, Any]:
|
|
"""Validate quality of a single React file"""
|
|
|
|
score = 10.0
|
|
issues = []
|
|
|
|
# Check for TypeScript usage
|
|
if file_path.endswith('.tsx') or file_path.endswith('.ts'):
|
|
if not re.search(self.quality_patterns["typescript_types"], content):
|
|
score -= 1.0
|
|
issues.append(f"Missing TypeScript types in {file_path}")
|
|
|
|
# Check for proper hooks usage
|
|
if 'component' in file_path.lower() or 'hook' in file_path.lower():
|
|
if not re.search(self.quality_patterns["proper_hooks"], content):
|
|
score -= 1.0
|
|
issues.append(f"Missing proper hooks usage in {file_path}")
|
|
|
|
# Check for error handling
|
|
if not re.search(self.quality_patterns["error_handling"], content):
|
|
score -= 1.5
|
|
issues.append(f"CRITICAL: No error handling in {file_path}")
|
|
|
|
# Check for loading states
|
|
if 'component' in file_path.lower():
|
|
if not re.search(self.quality_patterns["loading_states"], content):
|
|
score -= 1.0
|
|
issues.append(f"Missing loading states in {file_path}")
|
|
|
|
# Check for accessibility
|
|
if 'component' in file_path.lower():
|
|
if not re.search(self.quality_patterns["accessibility"], content):
|
|
score -= 0.5
|
|
issues.append(f"Missing accessibility features in {file_path}")
|
|
|
|
# Check for security patterns
|
|
if 'form' in file_path.lower() or 'input' in file_path.lower():
|
|
if not re.search(self.quality_patterns["security"], content):
|
|
score -= 1.0
|
|
issues.append(f"Missing security validation in {file_path}")
|
|
|
|
# Check for basic structure
|
|
if len(content.strip()) < 100:
|
|
score -= 3.0
|
|
issues.append(f"CRITICAL: File too short/incomplete {file_path}")
|
|
|
|
# Check for syntax issues (basic)
|
|
if content.count('{') != content.count('}'):
|
|
score -= 2.0
|
|
issues.append(f"CRITICAL: Bracket mismatch in {file_path}")
|
|
|
|
return {
|
|
"score": max(0, score),
|
|
"issues": issues,
|
|
"file_path": file_path
|
|
}
|
|
|
|
def _extract_react_contracts(self, code_files: Dict[str, str], features: List[str]) -> Dict[str, Any]:
|
|
"""Extract API contracts from React code"""
|
|
|
|
contracts = {
|
|
"api_calls": [],
|
|
"components_created": [],
|
|
"hooks_created": [],
|
|
"services_created": []
|
|
}
|
|
|
|
for file_path, content in code_files.items():
|
|
# Extract API calls
|
|
api_pattern = r'(?:fetch|axios|api)\s*\.\s*(?:get|post|put|delete)\s*\(\s*[\'"`]([^\'"`]+)[\'"`]'
|
|
api_matches = re.findall(api_pattern, content, re.IGNORECASE)
|
|
|
|
for endpoint in api_matches:
|
|
contracts["api_calls"].append({
|
|
"endpoint": endpoint,
|
|
"file": file_path,
|
|
"method": "unknown" # Could be enhanced to detect method
|
|
})
|
|
|
|
# Extract component exports
|
|
if file_path.endswith('.tsx'):
|
|
component_pattern = r'export\s+(?:default\s+)?(?:const|function)\s+(\w+)'
|
|
component_matches = re.findall(component_pattern, content)
|
|
|
|
for component in component_matches:
|
|
contracts["components_created"].append({
|
|
"name": component,
|
|
"file": file_path,
|
|
"features": features
|
|
})
|
|
|
|
# Extract custom hooks
|
|
if 'hook' in file_path.lower() or re.search(r'export\s+(?:const|function)\s+use\w+', content):
|
|
hook_pattern = r'export\s+(?:const|function)\s+(use\w+)'
|
|
hook_matches = re.findall(hook_pattern, content)
|
|
|
|
for hook in hook_matches:
|
|
contracts["hooks_created"].append({
|
|
"name": hook,
|
|
"file": file_path,
|
|
"features": features
|
|
})
|
|
|
|
return contracts
|
|
|
|
async def _build_improvement_prompt(self, current_result: HandlerResult,
|
|
quality_target: float) -> str:
|
|
"""Build improvement prompt for React code refinement"""
|
|
|
|
issues_text = "\n".join([
|
|
f"- {issue}" for issue in current_result.contracts.get("quality_issues", [])
|
|
])
|
|
|
|
return f"""IMPROVE this React code to achieve {quality_target}/10 quality.
|
|
|
|
CURRENT QUALITY: {current_result.quality_score}/10
|
|
TARGET QUALITY: {quality_target}/10
|
|
|
|
IDENTIFIED ISSUES:
|
|
{issues_text}
|
|
|
|
CURRENT CODE FILES:
|
|
{json.dumps(current_result.code_files, indent=2)}
|
|
|
|
IMPROVEMENT REQUIREMENTS:
|
|
1. Fix all critical issues (error handling, security, accessibility)
|
|
2. Enhance TypeScript types and interfaces
|
|
3. Improve component structure and reusability
|
|
4. Add comprehensive error boundaries
|
|
5. Implement proper loading states
|
|
6. Ensure accessibility compliance
|
|
7. Add input validation and sanitization
|
|
8. Optimize performance with React.memo, useMemo
|
|
9. Follow React best practices and patterns
|
|
10. Ensure all components are production-ready
|
|
|
|
CRITICAL: Return ONLY valid JSON. No explanations, no markdown, no code blocks.
|
|
|
|
Return ONLY the improved code in this JSON format:
|
|
{{
|
|
"file_path": "improved_complete_code"
|
|
}}
|
|
|
|
Make every improvement necessary to reach the quality target."""
|
|
|
|
async def _apply_improvements(self, current_result: HandlerResult,
|
|
improvement_prompt: str) -> HandlerResult:
|
|
"""Apply improvements to React code"""
|
|
|
|
try:
|
|
response = await self._claude_request_with_retry(improvement_prompt, max_tokens=8000)
|
|
response_text = response.content[0].text
|
|
|
|
# Parse improved code
|
|
improved_code = self._parse_react_response(response_text)
|
|
|
|
# Merge with existing code (keep files that weren't improved)
|
|
final_code = current_result.code_files.copy()
|
|
final_code.update(improved_code)
|
|
|
|
# Re-validate quality
|
|
quality_report = await self._validate_code_quality(final_code)
|
|
|
|
# Update result
|
|
improved_result = HandlerResult(
|
|
success=True,
|
|
handler_type=self.handler_type,
|
|
features_implemented=current_result.features_implemented,
|
|
code_files=final_code,
|
|
contracts=self._extract_react_contracts(final_code, current_result.features_implemented),
|
|
quality_score=quality_report["overall_score"],
|
|
tokens_used=current_result.tokens_used + (
|
|
response.usage.input_tokens + response.usage.output_tokens
|
|
if hasattr(response, 'usage') else 0
|
|
),
|
|
refinement_cycles=current_result.refinement_cycles
|
|
)
|
|
|
|
return improved_result
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ React improvement failed: {e}")
|
|
return current_result # Return original if improvement fails
|
|
|
|
async def _claude_request_with_retry(self, prompt: str, max_tokens: int = 4000, max_retries: int = 3):
|
|
"""Make Claude API request with retry logic"""
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
await asyncio.sleep(2 * attempt) # Progressive delay
|
|
|
|
message = self.claude_client.messages.create(
|
|
model="claude-3-5-sonnet-20241022",
|
|
max_tokens=max_tokens,
|
|
temperature=0.1,
|
|
messages=[{"role": "user", "content": prompt}]
|
|
)
|
|
|
|
return message
|
|
|
|
except Exception as e:
|
|
if "overloaded" in str(e) or "rate_limit" in str(e):
|
|
wait_time = 5 * (2 ** attempt)
|
|
logger.warning(f"⚠️ API overloaded, waiting {wait_time}s (attempt {attempt+1})")
|
|
await asyncio.sleep(wait_time)
|
|
else:
|
|
logger.error(f"❌ Claude API error: {e}")
|
|
if attempt == max_retries - 1:
|
|
raise e
|
|
|
|
raise Exception("Max retries exceeded for Claude API")
|
|
|
|
def _generate_basic_app_component(self) -> str:
|
|
"""Generate basic App component as fallback"""
|
|
return '''import React from 'react';
|
|
import './App.css';
|
|
|
|
const App: React.FC = () => {
|
|
return (
|
|
<div className="App">
|
|
<header className="App-header">
|
|
<h1>Generated React Application</h1>
|
|
<p>Your application components will be implemented here.</p>
|
|
</header>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;'''
|
|
|
|
def _generate_basic_index_file(self) -> str:
|
|
"""Generate basic index file as fallback"""
|
|
return '''import React from 'react';
|
|
import ReactDOM from 'react-dom/client';
|
|
import './index.css';
|
|
import App from './App';
|
|
|
|
const root = ReactDOM.createRoot(
|
|
document.getElementById('root') as HTMLElement
|
|
);
|
|
|
|
root.render(
|
|
<React.StrictMode>
|
|
<App />
|
|
</React.StrictMode>
|
|
);''' |