Files
aicyclingcoach-go/CL_implementation_guide.md
2025-09-12 07:32:32 -07:00

46 KiB

Junior Developer Implementation Guide

AI Cycling Coach - Critical Features Implementation

This guide provides step-by-step instructions to implement the missing core features identified in the codebase evaluation.


🎯 Implementation Phases Overview

Phase Focus Duration Difficulty
Phase 1 Backend Core APIs 2-3 weeks Medium
Phase 2 Frontend Core Features 3-4 weeks Medium
Phase 3 Integration & Testing 1-2 weeks Easy-Medium
Phase 4 Polish & Production 1-2 weeks Easy

Phase 1: Backend Core APIs Implementation

Step 1.1: Plan Generation Endpoint

File: backend/app/routes/plan.py

Add this endpoint to the existing router:

from app.schemas.plan import PlanGenerationRequest, PlanGenerationResponse
from app.services.ai_service import AIService, AIServiceError

@router.post("/generate", response_model=PlanGenerationResponse)
async def generate_plan(
    request: PlanGenerationRequest,
    db: AsyncSession = Depends(get_db)
):
    """Generate a new training plan using AI based on rules and goals."""
    try:
        # Fetch rules from database
        rules_query = select(Rule).where(Rule.id.in_(request.rule_ids))
        result = await db.execute(rules_query)
        rules = result.scalars().all()
        
        if len(rules) != len(request.rule_ids):
            raise HTTPException(status_code=404, detail="One or more rules not found")
        
        # Get plaintext rules
        rule_texts = [rule.rule_text for rule in rules]
        
        # Initialize AI service
        ai_service = AIService(db)
        
        # Generate plan
        plan_data = await ai_service.generate_plan(rule_texts, request.goals.dict())
        
        # Create plan record
        db_plan = Plan(
            jsonb_plan=plan_data,
            version=1,
            parent_plan_id=None
        )
        db.add(db_plan)
        await db.commit()
        await db.refresh(db_plan)
        
        return PlanGenerationResponse(
            plan=db_plan,
            generation_metadata={
                "rules_used": len(rules),
                "goals": request.goals.dict(),
                "generated_at": datetime.utcnow().isoformat()
            }
        )
        
    except AIServiceError as e:
        raise HTTPException(status_code=503, detail=f"AI service error: {str(e)}")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Plan generation failed: {str(e)}")

File: backend/app/schemas/plan.py

Add these new schemas:

from typing import Dict, List, Optional, Any
from pydantic import BaseModel, Field
from uuid import UUID

class TrainingGoals(BaseModel):
    """Training goals for plan generation."""
    primary_goal: str = Field(..., description="Primary training goal")
    target_weekly_hours: int = Field(..., ge=3, le=20, description="Target hours per week")
    fitness_level: str = Field(..., description="Current fitness level")
    event_date: Optional[str] = Field(None, description="Target event date (YYYY-MM-DD)")
    preferred_routes: List[int] = Field(default=[], description="Preferred route IDs")
    avoid_days: List[str] = Field(default=[], description="Days to avoid training")

class PlanGenerationRequest(BaseModel):
    """Request schema for plan generation."""
    rule_ids: List[UUID] = Field(..., description="Rule set IDs to apply")
    goals: TrainingGoals = Field(..., description="Training goals")
    duration_weeks: int = Field(4, ge=1, le=20, description="Plan duration in weeks")
    user_preferences: Optional[Dict[str, Any]] = Field(None, description="Additional preferences")

class PlanGenerationResponse(BaseModel):
    """Response schema for plan generation."""
    plan: Plan = Field(..., description="Generated training plan")
    generation_metadata: Dict[str, Any] = Field(..., description="Generation metadata")
    
    class Config:
        orm_mode = True

Add these endpoints after the existing routes:

from app.schemas.rule import NaturalLanguageRuleRequest, ParsedRuleResponse

@router.post("/parse-natural-language", response_model=ParsedRuleResponse)
async def parse_natural_language_rules(
    request: NaturalLanguageRuleRequest,
    db: AsyncSession = Depends(get_db)
):
    """Parse natural language text into structured training rules."""
    try:
        # Initialize AI service
        ai_service = AIService(db)
        
        # Parse rules using AI
        parsed_data = await ai_service.parse_rules_from_natural_language(
            request.natural_language_text
        )
        
        # Validate parsed rules
        validation_result = _validate_parsed_rules(parsed_data)
        
        return ParsedRuleResponse(
            parsed_rules=parsed_data,
            confidence_score=parsed_data.get("confidence", 0.0),
            suggestions=validation_result.get("suggestions", []),
            validation_errors=validation_result.get("errors", []),
            rule_name=request.rule_name
        )
        
    except AIServiceError as e:
        raise HTTPException(status_code=503, detail=f"AI parsing failed: {str(e)}")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Rule parsing failed: {str(e)}")

@router.post("/validate-rules")
async def validate_rule_consistency(
    rules_data: Dict[str, Any],
    db: AsyncSession = Depends(get_db)
):
    """Validate rule consistency and detect conflicts."""
    validation_result = _validate_parsed_rules(rules_data)
    return {
        "is_valid": len(validation_result.get("errors", [])) == 0,
        "errors": validation_result.get("errors", []),
        "warnings": validation_result.get("warnings", []),
        "suggestions": validation_result.get("suggestions", [])
    }

def _validate_parsed_rules(parsed_rules: Dict[str, Any]) -> Dict[str, List[str]]:
    """Validate parsed rules for consistency and completeness."""
    errors = []
    warnings = []
    suggestions = []
    
    # Check for required fields
    required_fields = ["max_rides_per_week", "min_rest_between_hard"]
    for field in required_fields:
        if field not in parsed_rules:
            errors.append(f"Missing required field: {field}")
    
    # Validate numeric ranges
    max_rides = parsed_rules.get("max_rides_per_week", 0)
    if max_rides > 7:
        errors.append("Maximum rides per week cannot exceed 7")
    elif max_rides < 1:
        errors.append("Must have at least 1 ride per week")
    
    # Check for conflicts
    max_hours = parsed_rules.get("max_duration_hours", 0)
    if max_rides and max_hours:
        avg_duration = max_hours / max_rides
        if avg_duration > 5:
            warnings.append("Very long average ride duration detected")
        elif avg_duration < 0.5:
            warnings.append("Very short average ride duration detected")
    
    # Provide suggestions
    if "weather_constraints" not in parsed_rules:
        suggestions.append("Consider adding weather constraints for outdoor rides")
    
    return {
        "errors": errors,
        "warnings": warnings,
        "suggestions": suggestions
    }

File: backend/app/schemas/rule.py

Replace the existing content with:

from pydantic import BaseModel, Field, validator
from typing import Optional, Dict, Any, List

class NaturalLanguageRuleRequest(BaseModel):
    """Request schema for natural language rule parsing."""
    natural_language_text: str = Field(
        ..., 
        min_length=10, 
        max_length=5000,
        description="Natural language rule description"
    )
    rule_name: str = Field(..., min_length=1, max_length=100, description="Rule set name")
    
    @validator('natural_language_text')
    def validate_text_content(cls, v):
        # Check for required keywords
        required_keywords = ['ride', 'week', 'hour', 'day', 'rest', 'training']
        if not any(keyword in v.lower() for keyword in required_keywords):
            raise ValueError("Text must contain training-related keywords")
        return v

class ParsedRuleResponse(BaseModel):
    """Response schema for parsed rules."""
    parsed_rules: Dict[str, Any] = Field(..., description="Structured rule data")
    confidence_score: Optional[float] = Field(None, ge=0.0, le=1.0, description="Parsing confidence")
    suggestions: List[str] = Field(default=[], description="Improvement suggestions")
    validation_errors: List[str] = Field(default=[], description="Validation errors")
    rule_name: str = Field(..., description="Rule set name")

class RuleBase(BaseModel):
    """Base rule schema."""
    name: str = Field(..., min_length=1, max_length=100)
    description: Optional[str] = Field(None, max_length=500)
    user_defined: bool = Field(True, description="Whether rule is user-defined")
    jsonb_rules: Dict[str, Any] = Field(..., description="Structured rule data")
    version: int = Field(1, ge=1, description="Rule version")
    parent_rule_id: Optional[int] = Field(None, description="Parent rule for versioning")

class RuleCreate(RuleBase):
    """Schema for creating new rules."""
    pass

class Rule(RuleBase):
    """Complete rule schema with database fields."""
    id: int
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True

Add these enhanced methods:

async def parse_rules_from_natural_language(self, natural_language: str) -> Dict[str, Any]:
    """Enhanced natural language rule parsing with better prompts."""
    prompt_template = await self.prompt_manager.get_active_prompt("rule_parsing")
    
    if not prompt_template:
        # Fallback prompt if none exists in database
        prompt_template = """
        Parse the following natural language training rules into structured JSON format.
        
        Input: "{user_rules}"
        
        Required output format:
        {{
            "max_rides_per_week": <number>,
            "min_rest_between_hard": <number>,
            "max_duration_hours": <number>,
            "intensity_limits": {{
                "max_zone_5_minutes_per_week": <number>,
                "max_consecutive_hard_days": <number>
            }},
            "weather_constraints": {{
                "min_temperature": <number>,
                "max_wind_speed": <number>,
                "no_rain": <boolean>
            }},
            "schedule_constraints": {{
                "preferred_days": [<day_names>],
                "avoid_days": [<day_names>]
            }},
            "confidence": <0.0-1.0>
        }}
        
        Extract specific numbers and constraints. If information is missing, omit the field.
        """
    
    prompt = prompt_template.format(user_rules=natural_language)
    response = await self._make_ai_request(prompt)
    parsed_data = self._parse_rules_response(response)
    
    # Add confidence scoring
    if "confidence" not in parsed_data:
        parsed_data["confidence"] = self._calculate_parsing_confidence(
            natural_language, parsed_data
        )
    
    return parsed_data

def _calculate_parsing_confidence(self, input_text: str, parsed_data: Dict) -> float:
    """Calculate confidence score for rule parsing."""
    confidence = 0.5  # Base confidence
    
    # Increase confidence for explicit numbers
    import re
    numbers = re.findall(r'\d+', input_text)
    if len(numbers) >= 2:
        confidence += 0.2
    
    # Increase confidence for key training terms
    training_terms = ['rides', 'hours', 'week', 'rest', 'recovery', 'training']
    found_terms = sum(1 for term in training_terms if term in input_text.lower())
    confidence += min(0.3, found_terms * 0.05)
    
    # Decrease confidence if parsed data is sparse
    if len(parsed_data) < 3:
        confidence -= 0.2
    
    return max(0.0, min(1.0, confidence))

Phase 2: Frontend Core Features Implementation

Step 2.1: Simplified Rules Management

File: frontend/src/pages/Rules.jsx

Replace with simplified plaintext rules interface:

import React, { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import RuleEditor from '../components/rules/RuleEditor';
import RulePreview from '../components/rules/RulePreview';
import RulesList from '../components/rules/RulesList';
import { useAuth } from '../context/AuthContext';
import * as ruleService from '../services/ruleService';

const Rules = () => {
  const { apiKey } = useAuth();
  const [activeTab, setActiveTab] = useState('list');
  const [rules, setRules] = useState([]);
  const [selectedRule, setSelectedRule] = useState(null);
  const [naturalLanguageText, setNaturalLanguageText] = useState('');
  const [parsedRules, setParsedRules] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    loadRules();
  }, []);

  const loadRules = async () => {
    try {
      const response = await ruleService.getRules();
      setRules(response.data);
    } catch (error) {
      console.error('Failed to load rules:', error);
      toast.error('Failed to load rules');
    }
  };

  const handleParseRules = async (parsedData) => {
    setParsedRules(parsedData);
    setActiveTab('preview');
  };

  const handleSaveRules = async (ruleName, finalRules) => {
    setIsLoading(true);
    try {
      const ruleData = {
        name: ruleName,
        jsonb_rules: finalRules,
        user_defined: true,
        version: 1
      };

      await ruleService.createRule(ruleData);
      toast.success('Rules saved successfully!');
      
      // Reset form and reload rules
      setNaturalLanguageText('');
      setParsedRules(null);
      setActiveTab('list');
      await loadRules();
    } catch (error) {
      console.error('Failed to save rules:', error);
      toast.error('Failed to save rules');
    } finally {
      setIsLoading(false);
    }
  };

  const handleEditRule = (rule) => {
    setSelectedRule(rule);
    setNaturalLanguageText(rule.description || '');
    setParsedRules(rule.jsonb_rules);
    setActiveTab('edit');
  };

  return (
    <div className="max-w-6xl mx-auto p-6">
      <div className="flex items-center justify-between mb-8">
        <div>
          <h1 className="text-3xl font-bold text-gray-900">Training Rules</h1>
          <p className="text-gray-600 mt-2">
            Define your training constraints and preferences using natural language
          </p>
        </div>
        
        <button
          onClick={() => setActiveTab('create')}
          className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
        >
          Create New Rules
        </button>
      </div>

      {/* Tab Navigation */}
      <div className="border-b border-gray-200 mb-6">
        <nav className="-mb-px flex space-x-8">
          {[
            { key: 'list', label: 'All Rules' },
            { key: 'create', label: 'Create Rules' },
            { key: 'preview', label: 'Preview' }
          ].map((tab) => (
            <button
              key={tab.key}
              onClick={() => setActiveTab(tab.key)}
              className={`py-2 px-1 border-b-2 font-medium text-sm ${
                activeTab === tab.key
                  ? 'border-blue-500 text-blue-600'
                  : 'border-transparent text-gray-500 hover:text-gray-700'
              }`}
            >
              {tab.label}
            </button>
          ))}
        </nav>
      </div>

      {/* Tab Content */}
      {activeTab === 'list' && (
        <RulesList
          rules={rules}
          onEdit={handleEditRule}
          onDelete={async (ruleId) => {
            try {
              await ruleService.deleteRule(ruleId);
              toast.success('Rule deleted');
              await loadRules();
            } catch (error) {
              toast.error('Failed to delete rule');
            }
          }}
        />
      )}

      {isEditing ? (
        <div className="space-y-6">
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-2">
              Rule Name
            </label>
            <input
              type="text"
              value={ruleName}
              onChange={(e) => setRuleName(e.target.value)}
              className="w-full p-3 border border-gray-300 rounded-lg"
            />
          </div>
          
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-2">
              Rule Text
            </label>
            <textarea
              value={ruleText}
              onChange={(e) => setRuleText(e.target.value)}
              rows={5}
              className="w-full p-3 border border-gray-300 rounded-lg"
              placeholder="Enter your training rules in plain text"
            />
          </div>
          
          <button
            onClick={handleSaveRule}
            className="bg-blue-600 text-white px-6 py-2 rounded-lg"
          >
            Save Rule
          </button>
        </div>
      ) : null}
    </div>
  );
};

export default Rules;

Create the missing component:

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import ReactJsonTree from 'react-json-tree';

const RulePreview = ({ naturalLanguageText, parsedRules, onSave, isLoading, onEdit }) => {
  const [ruleName, setRuleName] = useState('');
  const [editedRules, setEditedRules] = useState(parsedRules);
  const [showJsonEditor, setShowJsonEditor] = useState(false);

  const handleSave = () => {
    if (!ruleName.trim()) {
      alert('Please enter a rule name');
      return;
    }
    onSave(ruleName, editedRules);
  };

  const theme = {
    scheme: 'monokai',
    author: 'wimer hazenberg',
    base00: '#272822',
    base01: '#383830',
    base02: '#49483e',
    base03: '#75715e',
    base04: '#a59f85',
    base05: '#f8f8f2',
    base06: '#f5f4f1',
    base07: '#f9f8f5',
    base08: '#f92672',
    base09: '#fd971f',
    base0A: '#f4bf75',
    base0B: '#a6e22e',
    base0C: '#a1efe4',
    base0D: '#66d9ef',
    base0E: '#ae81ff',
    base0F: '#cc6633'
  };

  return (
    <div className="space-y-6">
      {/* Rule Name Input */}
      <div>
        <label className="block text-sm font-medium text-gray-700 mb-2">
          Rule Set Name
        </label>
        <input
          type="text"
          value={ruleName}
          onChange={(e) => setRuleName(e.target.value)}
          placeholder="e.g., Winter Training Rules"
          className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
        />
      </div>

      {/* Natural Language Summary */}
      <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
        <h3 className="text-lg font-semibold text-blue-800 mb-2">
          Original Input
        </h3>
        <p className="text-blue-700">{naturalLanguageText}</p>
      </div>

      {/* Parsed Rules Display */}
      <div className="bg-white border rounded-lg">
        <div className="flex items-center justify-between p-4 border-b">
          <h3 className="text-lg font-semibold">Parsed Rules</h3>
          <div className="flex space-x-2">
            <button
              onClick={() => setShowJsonEditor(!showJsonEditor)}
              className="px-3 py-1 bg-gray-100 text-gray-700 rounded hover:bg-gray-200"
            >
              {showJsonEditor ? 'View Tree' : 'Edit JSON'}
            </button>
            <button
              onClick={onEdit}
              className="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
            >
              Edit Input
            </button>
          </div>
        </div>

        <div className="p-4">
          {showJsonEditor ? (
            <textarea
              value={JSON.stringify(editedRules, null, 2)}
              onChange={(e) => {
                try {
                  setEditedRules(JSON.parse(e.target.value));
                } catch (err) {
                  // Handle JSON parsing errors
                }
              }}
              className="w-full h-64 p-3 border rounded font-mono text-sm"
            />
          ) : (
            <div className="bg-gray-900 rounded p-4 overflow-auto max-h-64">
              <ReactJsonTree
                data={editedRules}
                theme={theme}
                invertTheme={false}
                shouldExpandNode={() => true}
              />
            </div>
          )}
        </div>
      </div>

      {/* Validation Summary */}
      {parsedRules.confidence && (
        <div className="bg-green-50 border border-green-200 rounded-lg p-4">
          <div className="flex items-center justify-between">
            <span className="text-green-800 font-medium">Parsing Confidence</span>
            <div className="flex items-center">
              <div className="w-32 h-2 bg-green-200 rounded-full mr-3">
                <div 
                  className="h-full bg-green-600 rounded-full"
                  style={{ width: `${parsedRules.confidence * 100}%` }}
                />
              </div>
              <span className="text-green-800 font-mono">
                {(parsedRules.confidence * 100).toFixed(1)}%
              </span>
            </div>
          </div>
        </div>
      )}

      {/* Action Buttons */}
      <div className="flex space-x-4 pt-4 border-t">
        <button
          onClick={handleSave}
          disabled={isLoading || !ruleName.trim()}
          className={`px-6 py-2 rounded-lg font-medium ${
            isLoading || !ruleName.trim()
              ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
              : 'bg-green-600 text-white hover:bg-green-700'
          }`}
        >
          {isLoading ? 'Saving...' : 'Save Rules'}
        </button>
        
        <button
          onClick={onEdit}
          className="px-6 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
        >
          Back to Editor
        </button>
      </div>
    </div>
  );
};

RulePreview.propTypes = {
  naturalLanguageText: PropTypes.string.isRequired,
  parsedRules: PropTypes.object.isRequired,
  onSave: PropTypes.func.isRequired,
  isLoading: PropTypes.bool,
  onEdit: PropTypes.func.isRequired
};

export default RulePreview;

Step 2.2: Plan Generation Workflow

File: frontend/src/pages/PlanGeneration.jsx

Create the complete plan generation interface:

import React, { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import { useAuth } from '../context/AuthContext';
import GoalSelector from '../components/plans/GoalSelector';
import PlanParameters from '../components/plans/PlanParameters';
import ProgressTracker from '../components/ui/ProgressTracker';
import PlanTimeline from '../components/plans/PlanTimeline';
import * as planService from '../services/planService';
import * as ruleService from '../services/ruleService';

const PlanGeneration = () => {
  const { apiKey } = useAuth();
  const [currentStep, setCurrentStep] = useState(0);
  const [isGenerating, setIsGenerating] = useState(false);
  const [generatedPlan, setGeneratedPlan] = useState(null);
  
  // Form data
  const [selectedRules, setSelectedRules] = useState([]);
  const [goals, setGoals] = useState({
    primary_goal: '',
    target_weekly_hours: 8,
    fitness_level: 'intermediate',
    event_date: '',
    preferred_routes: [],
    avoid_days: []
  });
  const [parameters, setParameters] = useState({
    duration_weeks: 4,
    user_preferences: {}
  });
  
  const [availableRules, setAvailableRules] = useState([]);

  useEffect(() => {
    loadAvailableRules();
  }, []);

  const loadAvailableRules = async () => {
    try {
      const response = await ruleService.getRules();
      setAvailableRules(response.data);
    } catch (error) {
      console.error('Failed to load rules:', error);
      toast.error('Failed to load available rules');
    }
  };

  const steps = [
    { name: 'Select Rules', component: 'rules' },
    { name: 'Set Goals', component: 'goals' },
    { name: 'Parameters', component: 'parameters' },
    { name: 'Generate', component: 'generate' },
    { name: 'Review', component: 'review' }
  ];

  const handleNext = () => {
    if (currentStep < steps.length - 1) {
      setCurrentStep(currentStep + 1);
    }
  };

  const handlePrev = () => {
    if (currentStep > 0) {
      setCurrentStep(currentStep - 1);
    }
  };

  const handleGeneratePlan = async () => {
    setIsGenerating(true);
    try {
      const request = {
        rule_ids: selectedRules.map(rule => rule.id),
        goals: goals,
        duration_weeks: parameters.duration_weeks,
        user_preferences: parameters.user_preferences
      };

      const response = await planService.generatePlan(request);
      setGeneratedPlan(response.data);
      setCurrentStep(4); // Move to review step
      toast.success('Training plan generated successfully!');
    } catch (error) {
      console.error('Failed to generate plan:', error);
      toast.error('Failed to generate training plan');
    } finally {
      setIsGenerating(false);
    }
  };

  const renderStepContent = () => {
    switch (steps[currentStep].component) {
      case 'rules':
        return (
          <RulesSelection
            availableRules={availableRules}
            selectedRules={selectedRules}
            onSelectionChange={setSelectedRules}
          />
        );
      
      case 'goals':
        return (
          <GoalSelector
            goals={goals}
            onChange={setGoals}
          />
        );
      
      case 'parameters':
        return (
          <PlanParameters
            parameters={parameters}
            onChange={setParameters}
          />
        );
      
      case 'generate':
        return (
          <GenerationSummary
            selectedRules={selectedRules}
            goals={goals}
            parameters={parameters}
            onGenerate={handleGeneratePlan}
            isGenerating={isGenerating}
          />
        );
      
      case 'review':
        return generatedPlan ? (
          <PlanTimeline plan={generatedPlan.plan} mode="view" />
        ) : (
          <div>No plan generated</div>
        );
      
      default:
        return <div>Unknown step</div>;
    }
  };

  return (
    <div className="max-w-4xl mx-auto p-6">
      <div className="mb-8">
        <h1 className="text-3xl font-bold text-gray-900 mb-2">
          Generate Training Plan
        </h1>
        <p className="text-gray-600">
          Create a personalized training plan based on your rules and goals
        </p>
      </div>

      {/* Progress Tracker */}
      <ProgressTracker
        steps={steps.map(step => step.name)}
        currentStep={currentStep}
        completedSteps={currentStep}
      />

      {/* Step Content */}
      <div className="bg-white rounded-lg shadow-md p-6 mt-8">
        <h2 className="text-xl font-semibold mb-6">
          {steps[currentStep].name}
        </h2>
        
        {renderStepContent()}
      </div>

      {/* Navigation Buttons */}
      <div className="flex justify-between mt-8">
        <button
          onClick={handlePrev}
          disabled={currentStep === 0}
          className={`px-6 py-2 rounded-lg ${
            currentStep === 0
              ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
              : 'bg-gray-600 text-white hover:bg-gray-700'
          }`}
        >
          Previous
        </button>

        {currentStep < 3 ? (
          <button
            onClick={handleNext}
            disabled={!canProceed()}
            className={`px-6 py-2 rounded-lg ${
              !canProceed()
                ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
                : 'bg-blue-600 text-white hover:bg-blue-700'
            }`}
          >
            Next
          </button>
        ) : currentStep === 4 ? (
          <button
            onClick={() => toast.info('Plan saved successfully!')}
            className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
          >
            Save Plan
          </button>
        ) : null}
      </div>
    </div>
  );

  function canProceed() {
    switch (currentStep) {
      case 0: return selectedRules.length > 0;
      case 1: return goals.primary_goal && goals.target_weekly_hours > 0;
      case 2: return parameters.duration_weeks >= 1;
      default: return true;
    }
  }
};

// Supporting Components

const RulesSelection = ({ availableRules, selectedRules, onSelectionChange }) => {
  const handleRuleToggle = (rule) => {
    const isSelected = selectedRules.some(r => r.id === rule.id);
    if (isSelected) {
      onSelectionChange(selectedRules.filter(r => r.id !== rule.id));
    } else {
      onSelectionChange([...selectedRules, rule]);
    }
  };

  return (
    <div className="space-y-4">
      <p className="text-gray-600 mb-4">
        Select the rule sets that should apply to your training plan:
      </p>
      
      {availableRules.map(rule => (
        <div
          key={rule.id}
          className={`border rounded-lg p-4 cursor-pointer transition-all ${
            selectedRules.some(r => r.id === rule.id)
              ? 'border-blue-500 bg-blue-50'
              : 'border-gray-300 hover:border-gray-400'
          }`}
          onClick={() => handleRuleToggle(rule)}
        >
          <div className="flex items-center justify-between">
            <div>
              <h3 className="font-medium">{rule.name}</h3>
              <p className="text-sm text-gray-600 mt-1">
                {rule.description || 'No description'}
              </p>
            </div>
            <input
              type="checkbox"
              checked={selectedRules.some(r => r.id === rule.id)}
              onChange={() => {}} // Handled by div onClick
              className="w-5 h-5 text-blue-600"
            />
          </div>
        </div>
      ))}
    </div>
  );
};

const GenerationSummary = ({ selectedRules, goals, parameters, onGenerate, isGenerating }) => {
  return (
    <div className="space-y-6">
      <div className="bg-gray-50 rounded-lg p-4">
        <h3 className="font-medium mb-3">Plan Summary</h3>
        <div className="grid grid-cols-2 gap-4 text-sm">
          <div>
            <span className="font-medium">Rules Applied:</span>
            <span className="ml-2">{selectedRules.length} rule sets</span>
          </div>
          <div>
            <span className="font-medium">Primary Goal:</span>
            <span className="ml-2">{goals.primary_goal}</span>
          </div>
          <div>
            <span className="font-medium">Weekly Hours:</span>
            <span className="ml-2">{goals.target_weekly_hours}h</span>
          </div>
          <div>
            <span className="font-medium">Duration:</span>
            <span className="ml-2">{parameters.duration_weeks} weeks</span>
          </div>
        </div>
      </div>

      <button
        onClick={onGenerate}
        disabled={isGenerating}
        className={`w-full py-3 rounded-lg font-medium ${
          isGenerating
            ? 'bg-gray-300 text-gray-500'
            : 'bg-blue-600 text-white hover:bg-blue-700'
        }`}
      >
        {isGenerating ? 'Generating Plan...' : 'Generate Training Plan'}
      </button>
    </div>
  );
};

export default PlanGeneration;

Step 2.3: Service Layer Implementation

File: frontend/src/services/planService.js

Create the plan service for API communication:

import axios from 'axios';

const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';

// Create axios instance with default config
const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add auth interceptor
api.interceptors.request.use((config) => {
  const apiKey = localStorage.getItem('apiKey');
  if (apiKey) {
    config.headers['X-API-Key'] = apiKey;
  }
  return config;
});

// Add error handling interceptor
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('apiKey');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export const generatePlan = async (planRequest) => {
  try {
    const response = await api.post('/plans/generate', planRequest);
    return response;
  } catch (error) {
    console.error('Plan generation failed:', error);
    throw error;
  }
};

export const getPlans = async () => {
  return await api.get('/plans');
};

export const getPlan = async (planId) => {
  return await api.get(`/plans/${planId}`);
};

export const updatePlan = async (planId, planData) => {
  return await api.put(`/plans/${planId}`, planData);
};

export const deletePlan = async (planId) => {
  return await api.delete(`/plans/${planId}`);
};

export const getPlanEvolution = async (planId) => {
  return await api.get(`/workouts/plans/${planId}/evolution`);
};

Update the rule service with new endpoints:

import axios from 'axios';

const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';

const api = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add auth interceptor
api.interceptors.request.use((config) => {
  const apiKey = localStorage.getItem('apiKey');
  if (apiKey) {
    config.headers['X-API-Key'] = apiKey;
  }
  return config;
});

export const parseRule = async (naturalLanguageText, ruleName) => {
  try {
    const response = await api.post('/rules/parse-natural-language', {
      natural_language_text: naturalLanguageText,
      rule_name: ruleName
    });
    return response;
  } catch (error) {
    console.error('Rule parsing failed:', error);
    throw error;
  }
};

export const validateRules = async (rulesData) => {
  try {
    const response = await api.post('/rules/validate-rules', rulesData);
    return response;
  } catch (error) {
    console.error('Rule validation failed:', error);
    throw error;
  }
};

export const getRules = async () => {
  return await api.get('/rules');
};

export const getRule = async (ruleId) => {
  return await api.get(`/rules/${ruleId}`);
};

export const createRule = async (ruleData) => {
  return await api.post('/rules', ruleData);
};

export const updateRule = async (ruleId, ruleData) => {
  return await api.put(`/rules/${ruleId}`, ruleData);
};

export const deleteRule = async (ruleId) => {
  return await api.delete(`/rules/${ruleId}`);
};

Phase 3: Integration & Testing

Step 3.1: Component Testing

File: frontend/src/components/rules/__tests__/RuleEditor.test.jsx

Add comprehensive component tests:

import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import RuleEditor from '../RuleEditor';
import * as ruleService from '../../../services/ruleService';

// Mock the service
jest.mock('../../../services/ruleService');

describe('RuleEditor', () => {
  const mockProps = {
    value: '',
    onChange: jest.fn(),
    onParse: jest.fn()
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('renders correctly', () => {
    render(<RuleEditor {...mockProps} />);
    
    expect(screen.getByText('Natural Language Editor')).toBeInTheDocument();
    expect(screen.getByPlaceholderText(/Enter your training rules/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Parse Rules/ })).toBeInTheDocument();
  });

  it('validates input correctly', () => {
    render(<RuleEditor {...mockProps} />);
    
    const textarea = screen.getByPlaceholderText(/Enter your training rules/);
    const parseButton = screen.getByRole('button', { name: /Parse Rules/ });
    
    // Should be disabled for invalid input
    expect(parseButton).toBeDisabled();
    
    // Enter valid training rule
    fireEvent.change(textarea, {
      target: { value: 'Maximum 4 rides per week with at least one rest day between hard workouts' }
    });
    
    // Should be enabled for valid input
    expect(parseButton).toBeEnabled();
  });

  it('calls parse function when button clicked', async () => {
    const mockParseResponse = {
      data: {
        jsonRules: {
          max_rides_per_week: 4,
          min_rest_between_hard: 1
        }
      }
    };
    
    ruleService.parseRule.mockResolvedValue(mockParseResponse);
    
    render(<RuleEditor {...mockProps} />);
    
    const textarea = screen.getByPlaceholderText(/Enter your training rules/);
    const parseButton = screen.getByRole('button', { name: /Parse Rules/ });
    
    fireEvent.change(textarea, {
      target: { value: 'Maximum 4 rides per week with recovery between sessions' }
    });
    
    fireEvent.click(parseButton);
    
    await waitFor(() => {
      expect(mockProps.onParse).toHaveBeenCalledWith(mockParseResponse.data.jsonRules);
    });
  });

  it('shows template suggestions', () => {
    render(<RuleEditor {...mockProps} />);
    
    const templatesButton = screen.getByText('Templates');
    fireEvent.click(templatesButton);
    
    // Should show template suggestions
    expect(screen.getByText(/Maximum 4 rides per week/)).toBeInTheDocument();
  });
});

Step 3.2: Backend API Testing (Updated)

File: backend/tests/routes/test_plan.py

Add updated endpoint tests (removed rule parsing tests):

import pytest
from httpx import AsyncClient
from app.main import app
from app.models import Plan, Rule
from app.services.ai_service import AIService
from unittest.mock import AsyncMock, patch

@pytest.mark.asyncio
class TestPlanGeneration:
    
    async def test_generate_plan_success(self, async_client: AsyncClient, db_session, api_headers):
        """Test successful plan generation."""
        # Create test rule
        test_rule = Rule(
            name="Test Rules",
            jsonb_rules={"max_rides_per_week": 4, "min_rest_between_hard": 1},
            version=1
        )
        db_session.add(test_rule)
        await db_session.commit()
        await db_session.refresh(test_rule)
        
        # Mock AI service response
        mock_plan_data = {
            "plan_overview": {
                "duration_weeks": 4,
                "weekly_hours": 8,
                "focus": "endurance"
            },
            "weeks": [
                {
                    "week_number": 1,
                    "focus": "base_building",
                    "workouts": [
                        {
                            "day": "tuesday",
                            "type": "easy_ride",
                            "duration_minutes": 90
                        }
                    ]
                }
            ]
        }
        
        with patch.object(AIService, 'generate_plan', return_value=mock_plan_data):
            response = await async_client.post(
                "/plans/generate",
                json={
                    "rule_ids": [str(test_rule.id)],
                    "goals": {
                        "primary_goal": "endurance",
                        "target_weekly_hours": 8,
                        "fitness_level": "intermediate"
                    },
                    "duration_weeks": 4
                },
                headers=api_headers
            )
        
        assert response.status_code == 200
        data = response.json()
        assert "plan" in data
        assert data["plan"]["jsonb_plan"]["plan_overview"]["focus"] == "endurance"
    
    async def test_generate_plan_missing_rules(self, async_client: AsyncClient, api_headers):
        """Test plan generation with non-existent rules."""
        response = await async_client.post(
            "/plans/generate",
            json={
                "rule_ids": ["non-existent-id"],
                "goals": {
                    "primary_goal": "endurance",
                    "target_weekly_hours": 8,
                    "fitness_level": "intermediate"
                }
            },
            headers=api_headers
        )
        
        assert response.status_code == 404
        assert "not found" in response.json()["detail"].lower()
    
    async def test_generate_plan_invalid_goals(self, async_client: AsyncClient, api_headers):
        """Test plan generation with invalid goals."""
        response = await async_client.post(
            "/plans/generate",
            json={
                "rule_ids": [],
                "goals": {
                    "target_weekly_hours": -1  # Invalid value
                }
            },
            headers=api_headers
        )
        
        assert response.status_code == 422  # Validation error

<REMOVED>

Phase 4: Production Polish

Step 4.1: Error Handling & User Experience

File: frontend/src/components/GlobalErrorHandler.jsx

Create comprehensive error handling:

import React from 'react';
import { toast } from 'react-toastify';

class GlobalErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Global error caught:', error, errorInfo);
    
    // Log to monitoring service in production
    if (process.env.NODE_ENV === 'production') {
      // logErrorToService(error, errorInfo);
    }
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="min-h-screen flex items-center justify-center bg-gray-50">
          <div className="max-w-md w-full bg-white shadow-lg rounded-lg p-6">
            <div className="text-center">
              <div className="text-red-500 text-6xl mb-4">⚠️</div>
              <h1 className="text-2xl font-bold text-gray-900 mb-2">
                Something went wrong
              </h1>
              <p className="text-gray-600 mb-6">
                An unexpected error occurred. Please try refreshing the page.
              </p>
              <button
                onClick={() => window.location.reload()}
                className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
              >
                Refresh Page
              </button>
            </div>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

// Hook for handling API errors
export const useErrorHandler = () => {
  const handleError = (error) => {
    console.error('API Error:', error);
    
    if (error.response) {
      const status = error.response.status;
      const message = error.response.data?.detail || error.message;
      
      switch (status) {
        case 400:
          toast.error(`Invalid request: ${message}`);
          break;
        case 401:
          toast.error('Authentication failed. Please log in again.');
          localStorage.removeItem('apiKey');
          window.location.href = '/login';
          break;
        case 403:
          toast.error('Access denied. You do not have permission.');
          break;
        case 404:
          toast.error('Resource not found.');
          break;
        case 422:
          toast.error(`Validation error: ${message}`);
          break;
        case 500:
          toast.error('Server error. Please try again later.');
          break;
        case 503:
          toast.error('Service temporarily unavailable.');
          break;
        default:
          toast.error(`Error: ${message}`);
      }
    } else if (error.request) {
      toast.error('Network error. Please check your connection.');
    } else {
      toast.error('An unexpected error occurred.');
    }
  };

  return { handleError };
};

export default GlobalErrorBoundary;

Step 4.2: Performance Optimization

File: frontend/src/hooks/useDebounce.js

Add debouncing for better performance:

import { useState, useEffect } from 'react';

export function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage in RuleEditor component
export function useDebouncedValidation(input, validationFn, delay = 500) {
  const debouncedInput = useDebounce(input, delay);
  const [isValidating, setIsValidating] = useState(false);
  const [validationResult, setValidationResult] = useState(null);

  useEffect(() => {
    if (debouncedInput) {
      setIsValidating(true);
      validationFn(debouncedInput)
        .then(result => {
          setValidationResult(result);
          setIsValidating(false);
        })
        .catch(error => {
          console.error('Validation error:', error);
          setIsValidating(false);
        });
    }
  }, [debouncedInput, validationFn]);

  return { isValidating, validationResult };
}

🚀 Implementation Timeline & Checkpoints

Week 1-2: Backend Foundation

  • Implement plan generation endpoint
  • Add natural language rule parsing
  • Create comprehensive test suite
  • Checkpoint: Can generate plans via API

Week 3-4: Frontend Core

  • Build rules management interface
  • Create plan generation workflow
  • Add error handling and loading states
  • Checkpoint: Complete user workflows working

Week 5-6: Integration & Testing

  • Connect frontend to backend APIs
  • Add comprehensive error handling
  • Performance optimization
  • Checkpoint: Full feature integration

Week 7: Production Polish

  • UI/UX improvements
  • Performance monitoring
  • Documentation updates
  • Checkpoint: Production-ready application

📝 Junior Developer Tips

Common Pitfalls to Avoid:

  1. API Integration: Always handle loading states and errors
  2. State Management: Use React hooks properly, avoid stale closures
  3. Async/Await: Always wrap async operations in try-catch blocks
  4. Type Safety: Use PropTypes or TypeScript for better debugging
  5. Performance: Debounce user input for API calls

Testing Strategy:

  1. Start Small: Test individual components before integration
  2. Mock Services: Use jest mocks for external API calls
  3. Error Cases: Test both success and failure scenarios
  4. User Flows: Test complete user workflows end-to-end

Debugging Approach:

  1. Console Logging: Add structured logging for API calls
  2. React DevTools: Use for inspecting component state
  3. Network Tab: Monitor API requests and responses
  4. Error Boundaries: Implement proper error catching

This implementation guide provides a clear, step-by-step approach for a junior developer to successfully implement the missing core features while maintaining code quality and following best practices.