package ai import ( "encoding/json" "fmt" "strings" ) // BuildSystemPrompt creates the system message for DeepSeek func BuildSystemPrompt() string { return `You are an expert cycling coach with deep knowledge of power-based training, periodization, and workout design. Your task is to generate structured cycling workouts based on user data and goals. You must respond ONLY with valid JSON. POWER ZONES (% of FTP): - Recovery: 30-55% FTP - Endurance/Zone 2: 56-75% FTP - Tempo: 76-87% FTP - Sweet Spot: 88-94% FTP - Threshold/FTP: 95-105% FTP - VO2max: 106-120% FTP - Anaerobic: 121-150% FTP - Neuromuscular: >150% FTP WORKOUT STRUCTURE: - All workouts must have warmup, main intervals, and cooldown - Duration is in seconds - Power is expressed as decimal (0.65 = 65% FTP, 1.0 = 100% FTP) - Segment types: "warmup", "steadystate", "interval", "ramp", "cooldown", "rest", "freeride" - For steady state efforts, use "power" field - For variable efforts, use "power_low" and "power_high" fields TSS CALCULATION: TSS = (duration_seconds × NP² / FTP²) / 36 Approximate: duration_minutes × intensity_factor² TRAINING PRINCIPLES: 1. Progressive overload within user's current fitness 2. Balance intensity and volume 3. Include recovery when needed 4. Respect weekly hour constraints 5. Build on recent training load (CTL/ATL/TSB) 6. Vary workouts to prevent monotony 7. Include appropriate warmup and cooldown for all workouts EVENT-BASED PERIODIZATION (when a target event is provided): - Structure the plan around the target event date using standard cycling periodization: - BUILD phase: Progressive volume/intensity increase (furthest from event) - PEAK phase: Highest intensity, race-specific intervals - TAPER phase: Reduce volume while maintaining intensity - RACE DAY: Generate a "Race" type workout on the event date with pre-race notes - RECOVERY: Easy rides after race day - A-priority race: 7-10 day taper, reduce volume 30-50%, maintain intensity - B-priority race: 3-5 day lighter taper - C-priority race: No taper, treat as training race - On race day, create a workout with type "Race" and include race distance/event details in notes - If multiple events exist, avoid scheduling hard workouts within 2 days of any event NUTRITION GUIDANCE (when nutrition context is provided): - Include brief fueling notes in the "notes" field for each workout: - Pre-ride: what to eat 1-2 hours before (e.g., "Pre: oatmeal + banana, 400kcal") - During: fueling for rides >60min (e.g., "During: 60g carbs/hr, sports drink") - Post-ride: recovery nutrition within 30min (e.g., "Post: protein shake + rice, 500kcal") - For easy/recovery rides: lighter fueling notes - For hard/long rides: emphasize carb loading and during-ride nutrition - Keep fueling notes concise (one line each) OUTPUT FORMAT (JSON only - BE CONCISE): { "workouts": [ { "title": "Workout Name", "description": "Brief description", "type": "Endurance|Tempo|Threshold|VO2 Max|Recovery|Sprint", "scheduled_date": "YYYY-MM-DD", "duration": 3600, "segments": [ {"type": "warmup", "duration": 600, "power_low": 0.40, "power_high": 0.65}, {"type": "steadystate", "duration": 2400, "power": 0.65, "cadence": 85}, {"type": "cooldown", "duration": 600, "power_low": 0.65, "power_high": 0.40} ], "estimated_tss": 45.0, "notes": "Brief coaching note" } ], "rationale": "Brief plan overview" } IMPORTANT: Keep descriptions and notes VERY SHORT (max 10 words each). Response must be valid, complete JSON.` } // BuildUserPrompt creates the user message with context func BuildUserPrompt(ctx UserContext, req GenerateRequest) string { contextJSON, _ := json.MarshalIndent(ctx, "", " ") focusAreasStr := strings.Join(req.FocusAreas, ", ") // Build event context section eventSection := "" if ctx.TargetEvent != nil { eventSection = fmt.Sprintf(` TARGET EVENT: - Name: %s - Date: %s (%d days away) - Type: %s - Distance: %.0f km - Priority: %s-race - IMPORTANT: Periodize the plan to peak for this event. Apply appropriate taper based on priority. On event day (%s), generate a "Race" type workout with event details in notes. `, ctx.TargetEvent.Name, ctx.TargetEvent.Date, ctx.TargetEvent.DaysAway, ctx.TargetEvent.EventType, ctx.TargetEvent.Distance, ctx.TargetEvent.Priority, ctx.TargetEvent.Date, ) } if len(ctx.UpcomingEvents) > 0 { eventSection += "\nUPCOMING EVENTS (avoid hard workouts within 2 days of these):\n" for _, ev := range ctx.UpcomingEvents { eventSection += fmt.Sprintf("- %s (%s, %s-race, %d days away)\n", ev.Name, ev.Date, ev.Priority, ev.DaysAway) } } // Build nutrition context section nutritionSection := "" if ctx.Nutrition != nil { nutritionSection = fmt.Sprintf(` NUTRITION CONTEXT: - Goal: %s - Daily Calorie Target: %d kcal (before workout additions) - Macro Targets: Protein %dg, Carbs %dg, Fat %dg - Dietary Preference: %s - IMPORTANT: Include brief fueling notes (pre-ride, during, post-ride) in each workout's "notes" field. Adjust fueling intensity to match workout intensity. `, ctx.Nutrition.Goal, ctx.Nutrition.DailyCalories, ctx.Nutrition.ProteinG, ctx.Nutrition.CarbsG, ctx.Nutrition.FatG, ctx.Nutrition.DietaryPref, ) } return fmt.Sprintf(`Generate a %d-day training plan with the following requirements: USER CONTEXT: %s PLAN PARAMETERS: - Duration: %d days - Focus Areas: %s - Intensity Level: %s - Weekly Available Hours: %d - Start Date: %s - Include Rest Days: %v %s%s REQUIREMENTS: 1. Generate workouts that fit within %d weekly hours 2. Focus on: %s 3. Overall intensity: %s 4. Respect user's current fitness (FTP: %d, recent training load) 5. Include variety and progressive adaptation 6. Provide clear workout descriptions and coaching notes 7. Ensure total duration of segments matches workout duration 8. Use appropriate power zones for each workout type 9. Include proper warmup and cooldown for every workout IMPORTANT: - Return ONLY valid JSON in the exact format specified in the system prompt - Do not include any explanatory text before or after the JSON - Ensure all dates are sequential starting from %s - Calculate realistic TSS values for each workout - Segment durations must sum to workout duration (in seconds) - Power values should be between 0.30 and 2.50 (30%% to 250%% FTP) Return ONLY valid JSON in the exact format specified in the system prompt.`, req.PlanDuration, string(contextJSON), req.PlanDuration, focusAreasStr, req.IntensityLevel, req.WeeklyHours, req.StartDate, req.IncludeRest, eventSection, nutritionSection, req.WeeklyHours, focusAreasStr, req.IntensityLevel, ctx.FTP, req.StartDate, ) } // extractJSON attempts to extract JSON from response that might be wrapped in markdown func extractJSON(response string) string { // Remove markdown code blocks if present if idx := strings.Index(response, "```json"); idx != -1 { response = response[idx+7:] } else if idx := strings.Index(response, "```"); idx != -1 { response = response[idx+3:] } if idx := strings.Index(response, "```"); idx != -1 { response = response[:idx] } return strings.TrimSpace(response) }