Add interactive routine wizard, slider controls, value persistence, and timer pause
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:
Carlos Escalante
2026-03-20 22:29:06 -06:00
parent d7c1f8f677
commit 11e086166c
10 changed files with 613 additions and 130 deletions

View File

@@ -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(
*,