UNIVERSITY PROJECT // 4-PERSON TEAM @ SFU
SMARTCART
An AI-powered weekly meal planning platform built as a university group project at SFU. Uses Google Gemini to generate personalized meal plans with allergy safety verification and auto-regeneration. Features a grocery aggregation engine that deduplicates ingredients, performs unit conversions (tsp→tbsp→cup→ml), and subtracts pantry items.
TECH_STACK
MY_ROLE
Led the database architecture, REST API design, and full backend infrastructure across both iterations. In Iteration 2, I owned security hardening (CSRF, Spring Security filter chain, prompt injection prevention), allergy verification with auto-regeneration, balanced meal planning prompts, the protein/veggie/fruit weekly picker, meal swap (single + multi-select), and wrote 53 of the team's 72 automated tests.
KEY_FEATURES
AI Meal Plan Generation
Gemini generates full weekly meal plans (breakfast/lunch/dinner) with structured JSON schema enforcement, retry logic for incomplete responses, and nutritional plate model guidelines.
Allergy Safety Verification
Post-generation allergen scanning of every recipe ingredient with automatic re-generation of flagged meals. Unsafe slots are replaced or removed entirely.
Grocery Aggregation Engine
Merges duplicate ingredients across all recipes, converts between units (tsp→tbsp→cup→ml), subtracts pantry quantities, and auto-categorizes items.
Ingredient Normalization
Canonicalizes names ("boneless skinless chicken thighs" → "chicken thigh", "scallions" → "green onion"), strips descriptors, singularizes plurals.
SOURCE_CODE
public GeminiMealPlanDto generateMealPlan(...) {
// Auto-rotate: sample subset for variety
preferredProteins = sampleFromList(preferredProteins, 3);
preferredVegetables = sampleFromList(preferredVegetables, 5);
Set<MealSlot> requestedSlots = expectedSlots(mealSchedule);
LinkedHashMap<MealSlot, MealEntry> acceptedMeals = new LinkedHashMap<>();
List<MealSlot> remainingSlots = new ArrayList<>(requestedSlots);
// Retry loop: up to 3 attempts for incomplete plans
for (int attempt = 1; attempt <= 3 && !remainingSlots.isEmpty(); attempt++) {
String prompt = buildFullMealPlanPrompt(
pantryIngredients, servingSize, dietaryRestrictions,
allergies, preferredCuisines, remainingSlotSet, ...);
lastRaw = generateContent(prompt);
GeminiMealPlanDto dto = parseJson(lastRaw, GeminiMealPlanDto.class);
ExtractedMeals extracted = extractValidMeals(dto, remainingSlotSet);
acceptedMeals.putAll(extracted.acceptedMeals());
remainingSlots = extracted.missingSlots();
}
return buildMealPlan(acceptedMeals);
} static UnitInfo from(String rawUnit) {
String unit = normalizeUnitText(rawUnit);
return switch (unit) {
case "tsp", "teaspoon" -> new UnitInfo("volume", "tsp", 4.929);
case "tbsp", "tablespoon" -> new UnitInfo("volume", "tbsp", 14.787);
case "cup" -> new UnitInfo("volume", "cup", 236.588);
case "ml" -> new UnitInfo("volume", "ml", 1.0);
case "oz", "ounce" -> new UnitInfo("weight", "oz", 28.350);
case "lb", "pound" -> new UnitInfo("weight", "lb", 453.592);
case "g", "gram" -> new UnitInfo("weight", "g", 1.0);
default -> new UnitInfo("exact:" + unit, unit, 1.0);
};
}
double convertTo(double quantity, UnitInfo target) {
double baseQuantity = quantity * baseFactor;
return baseQuantity / target.baseFactor;
} // Post-generation allergy verification with auto-regeneration
Set<String> allergenSet = parseAllergenSet(effectiveAllergies);
for (MealEntry entry : mealPlan.meals()) {
if (!allergenSet.isEmpty()
&& recipeContainsAllergen(entry.recipe(), allergenSet)) {
log.warn("Allergy detected in {} {} — re-generating",
entry.dayOfWeek(), entry.mealType());
// Re-generate this single slot with stronger allergy prompt
GeminiRecipeDto replacement = geminiService
.generateSingleMeal(entry.dayOfWeek(), entry.mealType(),
effectiveAllergies, dietaryRestrictions, cuisines);
if (replacement != null
&& !recipeContainsAllergen(replacement, allergenSet)) {
Recipe recipe = persistRecipe(replacement, servingSize);
plan.getRecipes().add(new MealPlanRecipe(plan, recipe, day, meal));
} else {
removedMeals.add(entry.dayOfWeek() + " " + entry.mealType());
log.warn("Still unsafe — slot removed");
}
}
} ARCHITECTURE
AI Layer
- Gemini 2.0 Flash / 1.5 Pro
- Structured JSON schema enforcement
- 3-attempt retry for incomplete plans
- Post-generation allergen scanning
- Single-slot re-generation
Backend
- Spring Boot 4 REST API
- Spring Security + JDBC sessions
- PostgreSQL + Flyway migrations
- Unit conversion engine
- Ingredient normalization pipeline
Data Layer
- Pantry item management
- Ingredient deduplication
- Category auto-classification
- Docker + Render deployment