summaryrefslogtreecommitdiff
path: root/routers/grocery.py
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-08 03:24:36 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-08 03:24:36 -0700
commit3de7c5eed5ba262abf0d746211e33800db6d66df (patch)
tree6fddb5381fb178423eac34894add5b611babe300 /routers/grocery.py
parentf361e7599d9a11ad3397b7b6bffee151ab9bdde9 (diff)
Add recipe suggestions, chat tab, and major workflow improvements
- Replace 7-day grid menu with browsable recipe suggestion cards (swap, remove, make this) - Add Chat tab: conversational AI with full pantry/menu/grocery context - Grocery list works without a menu (pantry-only mode) - Individual grocery checkboxes auto-add items to pantry - Swap modal with optional preference input - User notes textarea on menu and grocery generation - Clear button for menu and grocery list - LLM notes/summary displayed after generation, persisted to DB - Favicon linked in HTML - Category dropdown styled for dark theme - System prompt configurable via SYSTEM_PROMPT in .env - Fix startup error (ollama_timeout default), DB migration for menu_plans.notes - Simplify: batch N+1 queries, extract _current_monday(), merge chat sync fns, asyncio.get_running_loop(), fix currentGroceryId bug, cap chat history Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'routers/grocery.py')
-rw-r--r--routers/grocery.py135
1 files changed, 93 insertions, 42 deletions
diff --git a/routers/grocery.py b/routers/grocery.py
index cde2068..17215d2 100644
--- a/routers/grocery.py
+++ b/routers/grocery.py
@@ -12,6 +12,11 @@ router = APIRouter(prefix="/api/grocery", tags=["grocery"])
ai_router = APIRouter(prefix="/api/ai", tags=["ai"])
+def _current_monday():
+ today = datetime.utcnow().date()
+ return today - timedelta(days=today.weekday())
+
+
@router.get("")
async def list_grocery_lists(db: Session = Depends(get_db)):
"""List all grocery lists."""
@@ -22,8 +27,7 @@ async def list_grocery_lists(db: Session = Depends(get_db)):
@router.get("/current")
async def get_current_grocery_list(db: Session = Depends(get_db)):
"""Get grocery list for current week."""
- today = datetime.utcnow().date()
- monday = today - timedelta(days=today.weekday())
+ monday = _current_monday()
grocery_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first()
if not grocery_list:
@@ -41,57 +45,49 @@ async def get_grocery_list(id: int, db: Session = Depends(get_db)):
@router.post("/generate")
-async def generate_grocery_list(db: Session = Depends(get_db)):
+async def generate_grocery_list(request: dict = Body(default={}), db: Session = Depends(get_db)):
"""Generate a grocery list for the current week."""
+ user_notes = request.get("user_notes") if request else None
+
# Get current week's Monday
- today = datetime.utcnow().date()
- monday = today - timedelta(days=today.weekday())
+ monday = _current_monday()
# Fetch current MenuPlan
menu_plan = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
- if not menu_plan:
- raise HTTPException(status_code=404, detail="No menu plan for current week. Generate a menu first.")
- # Parse the plan JSON
- try:
- plan_dict = json.loads(menu_plan.plan)
- except json.JSONDecodeError:
- raise HTTPException(status_code=500, detail="Invalid menu plan data")
+ pantry_context = pantry_service.build_pantry_context(db)
- # Collect all recipe IDs and fetch recipes
- recipe_ids = set()
- for day_meals in plan_dict.values():
- if isinstance(day_meals, dict):
- for recipe_id in day_meals.values():
- if isinstance(recipe_id, int):
- recipe_ids.add(recipe_id)
+ try:
+ if menu_plan:
+ # Parse the plan JSON (now a flat array of recipe IDs)
+ try:
+ recipe_ids = json.loads(menu_plan.plan)
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Invalid menu plan data")
- recipes = db.query(Recipe).filter(Recipe.id.in_(recipe_ids)).all() if recipe_ids else []
- recipe_map = {r.id: r for r in recipes}
+ recipes = db.query(Recipe).filter(Recipe.id.in_(recipe_ids)).all() if recipe_ids else []
+ recipe_map = {r.id: r for r in recipes}
- # Build menu_plan_for_ai
- menu_plan_for_ai = {}
- for day, day_meals in plan_dict.items():
- menu_plan_for_ai[day] = {}
- for meal_type, recipe_id in day_meals.items():
- recipe = recipe_map.get(recipe_id)
- if recipe:
- try:
- ingredients = json.loads(recipe.ingredients)
- except (json.JSONDecodeError, TypeError):
- ingredients = []
- menu_plan_for_ai[day][meal_type] = {
- "name": recipe.name,
- "ingredients": ingredients,
- }
+ recipes_for_ai = []
+ for recipe_id in recipe_ids:
+ recipe = recipe_map.get(recipe_id)
+ if recipe:
+ try:
+ ingredients = json.loads(recipe.ingredients)
+ except (json.JSONDecodeError, TypeError):
+ ingredients = []
+ recipes_for_ai.append({
+ "name": recipe.name,
+ "meal_type": recipe.meal_type,
+ "ingredients": ingredients,
+ })
- # Build pantry context and generate grocery list
- try:
- pantry_context = pantry_service.build_pantry_context(db)
- ai_result = await ai_service.generate_grocery_list(menu_plan_for_ai, pantry_context)
+ ai_result = await ai_service.generate_grocery_list(recipes_for_ai, pantry_context, user_notes)
+ else:
+ ai_result = await ai_service.generate_grocery_list_from_pantry(pantry_context, user_notes)
except ValueError as e:
raise HTTPException(status_code=503, detail=str(e))
- except (ConnectionError, Exception) as e:
+ except Exception as e:
raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
# Upsert GroceryList
@@ -122,6 +118,18 @@ async def generate_grocery_list(db: Session = Depends(get_db)):
}
+@router.delete("/current")
+async def delete_current_grocery_list(db: Session = Depends(get_db)):
+ """Delete the grocery list for the current week."""
+ monday = _current_monday()
+ grocery_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first()
+ if not grocery_list:
+ raise HTTPException(status_code=404, detail="No grocery list for current week")
+ db.delete(grocery_list)
+ db.commit()
+ return {"status": "deleted"}
+
+
@router.put("/{id}/purchased")
async def mark_purchased(id: int, db: Session = Depends(get_db)):
"""Mark a grocery list as purchased and update pantry."""
@@ -175,7 +183,7 @@ async def suggest_recipe_endpoint(db: Session = Depends(get_db)):
return result
except ValueError as e:
raise HTTPException(status_code=503, detail=str(e))
- except (ConnectionError, Exception) as e:
+ except Exception as e:
raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
@@ -184,3 +192,46 @@ async def list_models():
"""List available Ollama models."""
models = await ai_service.get_available_models()
return {"models": models}
+
+
+@ai_router.post("/chat")
+async def chat_endpoint(request: dict = Body(default={}), db: Session = Depends(get_db)):
+ """Chat with Commis using current kitchen context."""
+ message = request.get("message", "").strip()
+ if not message:
+ raise HTTPException(status_code=400, detail="Message is required")
+
+ history = request.get("history", [])
+
+ # Build pantry context
+ pantry_context = pantry_service.build_pantry_context(db)
+
+ # Get current week's menu recipes
+ monday = _current_monday()
+
+ menu_plan = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
+ menu_context = []
+ if menu_plan:
+ try:
+ plan_ids = json.loads(menu_plan.plan)
+ recipes = db.query(Recipe).filter(Recipe.id.in_(plan_ids)).all() if plan_ids else []
+ menu_context = [{"name": r.name, "meal_type": r.meal_type} for r in recipes]
+ except Exception:
+ menu_context = []
+
+ # Get current grocery list items
+ grocery_list = db.query(GroceryList).filter(GroceryList.generated_for == monday).first()
+ grocery_context = []
+ if grocery_list:
+ try:
+ grocery_context = json.loads(grocery_list.items)
+ except Exception:
+ grocery_context = []
+
+ try:
+ response = await ai_service.chat_with_commis(message, history, pantry_context, menu_context, grocery_context)
+ return {"response": response}
+ except ValueError as e:
+ raise HTTPException(status_code=503, detail=str(e))
+ except Exception as e:
+ raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")