mirror of
https://github.com/escalante29/healthy-fit.git
synced 2026-03-21 12:28:46 +01:00
Add interactive routine wizard, slider controls, value persistence, and timer pause
All checks were successful
Deploy to VPS / deploy (push) Successful in 24s
All checks were successful
Deploy to VPS / deploy (push) Successful in 24s
- Interactive routine generation wizard with AI refinement loop (generate-draft, refine, save-draft endpoints + RoutineWizard modal component) - Replace +/- stepper buttons with slider controls for reps/weight during workout - Persist user-modified reps/weight across sets of the same exercise - Add pause/resume by tapping timer dials, with back-button confirmation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,3 +55,47 @@ class KettlebellModule(dspy.Module):
|
||||
|
||||
|
||||
kettlebell_module = KettlebellModule()
|
||||
|
||||
|
||||
class RefineKettlebellSession(dspy.Signature):
|
||||
"""Refine an existing kettlebell workout session based on user feedback.
|
||||
|
||||
You are given the current session and the user's feedback. Apply the requested changes
|
||||
while keeping the rest of the session intact. Maintain proper exercise sequencing,
|
||||
warm-up/cooldown structure, and ensure total work time still fits the target duration.
|
||||
"""
|
||||
|
||||
current_session: str = dspy.InputField(desc="JSON representation of the current session")
|
||||
user_feedback: str = dspy.InputField(desc="User's requested changes to the session")
|
||||
user_profile: str = dspy.InputField(desc="User details including age, weight, fitness level, and goals")
|
||||
available_weights_kg: str = dspy.InputField(desc="Comma-separated list of available kettlebell weights in kg")
|
||||
focus: str = dspy.InputField(desc="Session focus: strength, conditioning, mobility, fat loss, etc.")
|
||||
duration_minutes: int = dspy.InputField(desc="Target session duration in minutes")
|
||||
session: KettlebellSessionOutput = dspy.OutputField(desc="Refined structured kettlebell session")
|
||||
|
||||
|
||||
class KettlebellRefineModule(dspy.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.refine = dspy.ChainOfThought(RefineKettlebellSession)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
current_session: str,
|
||||
user_feedback: str,
|
||||
user_profile: str,
|
||||
available_weights_kg: str,
|
||||
focus: str,
|
||||
duration_minutes: int,
|
||||
):
|
||||
return self.refine(
|
||||
current_session=current_session,
|
||||
user_feedback=user_feedback,
|
||||
user_profile=user_profile,
|
||||
available_weights_kg=available_weights_kg,
|
||||
focus=focus,
|
||||
duration_minutes=duration_minutes,
|
||||
)
|
||||
|
||||
|
||||
kettlebell_refine_module = KettlebellRefineModule()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
@@ -6,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.ai.kettlebell import kettlebell_module
|
||||
from app.ai.kettlebell import kettlebell_module, kettlebell_refine_module
|
||||
from app.api import deps
|
||||
from app.models.kettlebell import KettlebellSession, KettlebellSetLog
|
||||
|
||||
@@ -19,6 +20,39 @@ class KettlebellGenerateRequest(BaseModel):
|
||||
available_weights: list[float]
|
||||
|
||||
|
||||
class DraftExercise(BaseModel):
|
||||
order: int
|
||||
name: str
|
||||
description: str
|
||||
sets: int
|
||||
reps: int
|
||||
duration_seconds: int
|
||||
weight_kg: float
|
||||
rest_seconds: int
|
||||
coaching_tip: str
|
||||
|
||||
|
||||
class DraftSession(BaseModel):
|
||||
title: str
|
||||
focus: str
|
||||
total_duration_min: int
|
||||
difficulty: str
|
||||
exercises: list[DraftExercise]
|
||||
notes: str
|
||||
|
||||
|
||||
class RefineRequest(BaseModel):
|
||||
draft: DraftSession
|
||||
feedback: str
|
||||
focus: str
|
||||
duration_minutes: int
|
||||
available_weights: list[float]
|
||||
|
||||
|
||||
class SaveDraftRequest(BaseModel):
|
||||
draft: DraftSession
|
||||
|
||||
|
||||
class LogSetRequest(BaseModel):
|
||||
exercise_order: int
|
||||
set_number: int
|
||||
@@ -178,6 +212,102 @@ def get_analytics(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/generate-draft", response_model=DraftSession)
|
||||
def generate_draft(
|
||||
*,
|
||||
current_user: deps.CurrentUser,
|
||||
request: KettlebellGenerateRequest,
|
||||
) -> Any:
|
||||
"""Generate a draft kettlebell session (not saved to DB) for review."""
|
||||
try:
|
||||
user_profile = (
|
||||
f"Age: {current_user.age or 'unknown'}, "
|
||||
f"Gender: {current_user.gender or 'unknown'}, "
|
||||
f"Weight: {current_user.weight or 'unknown'} kg, "
|
||||
f"Height: {current_user.height or 'unknown'} cm"
|
||||
)
|
||||
available_weights_kg = ", ".join(str(w) for w in sorted(request.available_weights))
|
||||
generated = kettlebell_module(
|
||||
user_profile=user_profile,
|
||||
available_weights_kg=available_weights_kg,
|
||||
focus=request.focus,
|
||||
duration_minutes=request.duration_minutes,
|
||||
)
|
||||
s = generated.session
|
||||
return DraftSession(
|
||||
title=s.title,
|
||||
focus=s.focus,
|
||||
total_duration_min=s.total_duration_min,
|
||||
difficulty=s.difficulty,
|
||||
exercises=[DraftExercise(**ex.model_dump()) for ex in s.exercises],
|
||||
notes=s.notes,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/refine", response_model=DraftSession)
|
||||
def refine_draft(
|
||||
*,
|
||||
current_user: deps.CurrentUser,
|
||||
request: RefineRequest,
|
||||
) -> Any:
|
||||
"""Refine a draft session based on user feedback."""
|
||||
try:
|
||||
user_profile = (
|
||||
f"Age: {current_user.age or 'unknown'}, "
|
||||
f"Gender: {current_user.gender or 'unknown'}, "
|
||||
f"Weight: {current_user.weight or 'unknown'} kg, "
|
||||
f"Height: {current_user.height or 'unknown'} cm"
|
||||
)
|
||||
available_weights_kg = ", ".join(str(w) for w in sorted(request.available_weights))
|
||||
current_session_json = json.dumps(request.draft.model_dump(), indent=2)
|
||||
generated = kettlebell_refine_module(
|
||||
current_session=current_session_json,
|
||||
user_feedback=request.feedback,
|
||||
user_profile=user_profile,
|
||||
available_weights_kg=available_weights_kg,
|
||||
focus=request.focus,
|
||||
duration_minutes=request.duration_minutes,
|
||||
)
|
||||
s = generated.session
|
||||
return DraftSession(
|
||||
title=s.title,
|
||||
focus=s.focus,
|
||||
total_duration_min=s.total_duration_min,
|
||||
difficulty=s.difficulty,
|
||||
exercises=[DraftExercise(**ex.model_dump()) for ex in s.exercises],
|
||||
notes=s.notes,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/save-draft", response_model=KettlebellSession)
|
||||
def save_draft(
|
||||
*,
|
||||
current_user: deps.CurrentUser,
|
||||
request: SaveDraftRequest,
|
||||
session: Session = Depends(deps.get_session),
|
||||
) -> Any:
|
||||
"""Save a finalized draft as a new session."""
|
||||
draft = request.draft
|
||||
kb_session = KettlebellSession(
|
||||
user_id=current_user.id,
|
||||
title=draft.title,
|
||||
focus=draft.focus,
|
||||
exercises={"exercises": [ex.model_dump() for ex in draft.exercises]},
|
||||
total_duration_min=draft.total_duration_min,
|
||||
difficulty=draft.difficulty,
|
||||
notes=draft.notes,
|
||||
status="generated",
|
||||
)
|
||||
session.add(kb_session)
|
||||
session.commit()
|
||||
session.refresh(kb_session)
|
||||
return kb_session
|
||||
|
||||
|
||||
@router.post("/generate", response_model=KettlebellSession)
|
||||
def generate_session(
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user