Backend:
- Add Groq LLM client (llama-3.3-70b) for recipe generation with JSON
retry strategy (retries only on parse errors, not API errors)
- Add Pexels client for parallel photo search per recipe
- Add saved_recipes table (migration 004) with JSONB fields
- Add GET /recommendations endpoint (profile-aware prompt building)
- Add POST/GET/GET{id}/DELETE /saved-recipes CRUD endpoints
- Wire gemini, pexels, recommendation, savedrecipe packages in main.go
Flutter:
- Add Recipe, SavedRecipe models with json_serializable
- Add RecipeService (getRecommendations, getSavedRecipes, save, delete)
- Add RecommendationsNotifier and SavedRecipesNotifier (Riverpod)
- Add RecommendationsScreen with skeleton loading and refresh FAB
- Add RecipeDetailScreen (SliverAppBar, nutrition tooltip, steps with timer)
- Add SavedRecipesScreen with Dismissible swipe-to-delete and empty state
- Update RecipesScreen to TabBar (Recommendations / Saved)
- Add /recipe-detail route outside ShellRoute (no bottom nav)
- Extend ApiClient with getList() and deleteVoid()
Project:
- Add CLAUDE.md with English-only rule for comments and commit messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
10 KiB
Итерация 1: AI-рекомендации рецептов
Цель: реализовать ключевую функцию — персонализированные рецепты, сгенерированные Gemini с фотографиями из Pexels, и возможность их сохранять.
Зависимости: Итерация 0 (авторизация, профиль, БД).
Ориентир: Summary.md
Структура задач
1.1 Backend: Gemini-клиент
├── 1.1.1 Пакет internal/gemini (интерфейс + адаптер)
├── 1.1.2 GenerateRecipes(ctx, prompt) → []Recipe
└── 1.1.3 Retry-стратегия (невалидный JSON → повтор с уточнением)
1.2 Backend: Pexels-клиент
├── 1.2.1 Пакет internal/pexels
└── 1.2.2 SearchPhoto(ctx, query) → image_url
1.3 Backend: saved_recipes
├── 1.3.1 Миграция: таблица saved_recipes
├── 1.3.2 Repository (CRUD)
└── 1.3.3 Service layer
1.4 Backend: эндпоинты рекомендаций
├── 1.4.1 GET /recommendations?count=5
└── 1.4.2 Формирование промпта из профиля пользователя
1.5 Backend: эндпоинты saved_recipes
├── 1.5.1 POST /saved-recipes
├── 1.5.2 GET /saved-recipes
├── 1.5.3 GET /saved-recipes/{id}
└── 1.5.4 DELETE /saved-recipes/{id}
1.6 Flutter: экран рекомендаций
├── 1.6.1 RecommendationsScreen (список карточек)
├── 1.6.2 Skeleton-загрузка (2–4 сек)
└── 1.6.3 Кнопка сохранить (♡)
1.7 Flutter: карточка рецепта
└── 1.7.1 RecipeDetailScreen (фото, КБЖУ≈, ингредиенты, шаги)
1.8 Flutter: сохранённые рецепты
└── 1.8.1 SavedRecipesScreen (список с удалением)
1.1 Gemini-клиент
1.1.1 Структура пакета
internal/
└── gemini/
├── client.go # HTTP-клиент к Gemini API
├── recipe.go # GenerateRecipes()
└── client_test.go
1.1.2 Интерфейс
type RecipeGenerator interface {
GenerateRecipes(ctx context.Context, req RecipeRequest) ([]Recipe, error)
}
type RecipeRequest struct {
UserGoal string // "weight_loss" | "maintain" | "gain"
DailyCalories int
Restrictions []string // ["gluten_free", "vegetarian"]
CuisinePrefs []string // ["russian", "asian"]
Count int
}
type Recipe struct {
Title string
Description string
Cuisine string
Difficulty string // "easy" | "medium" | "hard"
PrepTimeMin int
CookTimeMin int
Servings int
ImageQuery string // EN, для Pexels
Ingredients []Ingredient
Steps []Step
Tags []string
Nutrition NutritionInfo // приблизительно
}
1.1.3 Промпт для рекомендаций
Ты — диетолог-повар. Предложи {N} рецептов на русском языке.
Профиль пользователя:
- Цель: {goal_ru}
- Дневная норма калорий: {calories} ккал
- Ограничения: {restrictions или "нет"}
- Предпочтения: {cuisines или "любые"}
Требования к каждому рецепту:
- Калорийность на порцию: не более {per_meal_calories} ккал
- Время приготовления: до 60 минут
- Укажи КБЖУ на порцию (приблизительно)
Верни ТОЛЬКО валидный JSON-массив без markdown-обёртки:
[{
"title": "Название",
"description": "2-3 предложения",
"cuisine": "russian|asian|european|mediterranean|american|other",
"difficulty": "easy|medium|hard",
"prep_time_min": 10,
"cook_time_min": 20,
"servings": 2,
"image_query": "dish name ingredients style",
"ingredients": [{"name": "Куриная грудка", "amount": 300, "unit": "г"}],
"steps": [{"number": 1, "description": "...", "timer_seconds": null}],
"tags": ["высокий белок"],
"nutrition_per_serving": {
"calories": 420, "protein_g": 48, "fat_g": 12, "carbs_g": 18
}
}]
1.1.4 Retry-стратегия
При получении невалидного JSON:
- Первая попытка: обычный промпт
- При ошибке парсинга — повтор с явным уточнением: «Предыдущий ответ не был валидным JSON. Верни ТОЛЬКО массив JSON без какого-либо текста до или после.»
- Максимум 3 попытки, затем HTTP 503
1.2 Pexels-клиент
1.2.1 Пакет
internal/
└── pexels/
├── client.go # HTTP-клиент к Pexels API
└── client_test.go
1.2.2 Поиск фото
type PhotoSearcher interface {
SearchPhoto(ctx context.Context, query string) (string, error) // image_url
}
- Запрос:
GET https://api.pexels.com/v1/search?query={query}&per_page=1&orientation=landscape - Авторизация:
Authorization: {PEXELS_API_KEY} - Берём
photos[0].src.medium(~1200×630) - При пустом ответе — возвращаем дефолтное фото (placeholder)
1.2.3 Параллельный запрос для рецептов
// Для каждого рецепта — параллельный запрос к Pexels
var wg sync.WaitGroup
for i, recipe := range recipes {
wg.Add(1)
go func(i int, r Recipe) {
defer wg.Done()
url, _ := pexels.SearchPhoto(ctx, r.ImageQuery)
recipes[i].ImageURL = url
}(i, recipe)
}
wg.Wait()
1.3 Миграция: saved_recipes
-- migrations/002_create_saved_recipes.sql
-- +goose Up
CREATE TABLE saved_recipes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
cuisine TEXT,
difficulty TEXT,
prep_time_min INT,
cook_time_min INT,
servings INT,
image_url TEXT,
ingredients JSONB NOT NULL DEFAULT '[]',
steps JSONB NOT NULL DEFAULT '[]',
tags JSONB NOT NULL DEFAULT '[]',
nutrition JSONB, -- {calories, protein_g, fat_g, carbs_g}
source TEXT NOT NULL DEFAULT 'ai',
saved_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_saved_recipes_user_id ON saved_recipes(user_id);
CREATE INDEX idx_saved_recipes_saved_at ON saved_recipes(user_id, saved_at DESC);
-- +goose Down
DROP TABLE saved_recipes;
1.4 GET /recommendations
Flow
1. Получить JWT из middleware → user_id
2. SELECT users WHERE id=$1 → профиль (goal, daily_calories, preferences)
3. Формирование промпта из профиля
4. gemini.GenerateRecipes(ctx, req) → []Recipe (~1–3 сек)
5. Для каждого рецепта: pexels.SearchPhoto(ctx, image_query) (параллельно)
6. Вернуть JSON-массив рецептов с image_url
Request / Response
GET /recommendations?count=5
Response 200:
[{
"title": "Куриная грудка с овощами",
"description": "Лёгкое блюдо высокого белка...",
"cuisine": "european",
"difficulty": "easy",
"prep_time_min": 10,
"cook_time_min": 25,
"servings": 2,
"image_url": "https://images.pexels.com/...",
"ingredients": [...],
"steps": [...],
"tags": ["высокий белок", "без глютена"],
"nutrition_per_serving": {
"calories": 420,
"protein_g": 48,
"fat_g": 12,
"carbs_g": 18,
"approximate": true
}
}]
1.5 Эндпоинты saved_recipes
| Метод | Путь | Описание |
|---|---|---|
| POST | /saved-recipes |
Сохранить рецепт из рекомендаций |
| GET | /saved-recipes |
Список сохранённых (по saved_at DESC) |
| GET | /saved-recipes/{id} |
Один сохранённый рецепт |
| DELETE | /saved-recipes/{id} |
Удалить из сохранённых |
POST /saved-recipes принимает полный объект рецепта (уже с image_url) и сохраняет в БД. Gemini и Pexels не вызываются.
1.6–1.8 Flutter
RecommendationsScreen
- При открытии вкладки:
GET /recommendations?count=5 - Skeleton-анимация пока идёт запрос
- Карточка: фото (CachedNetworkImage), название, КБЖУ≈, время, сложность
- Иконка ♡: тап → POST /saved-recipes → haptic feedback, иконка заполняется
- Кнопка 🔄 в AppBar: перегенерировать рекомендации
RecipeDetailScreen
- Фото сверху (hero animation)
- Метаданные: время, сложность, кухня
- КБЖУ-блок с пометкой «≈ приблизительно» (тап → tooltip)
- Список ингредиентов с количеством
- Нумерованные шаги (с таймером если
timer_seconds != null) - Кнопка «Сохранить» / «Сохранено»
SavedRecipesScreen
- GET /saved-recipes (пагинация)
- Карточки с фото, название, КБЖУ
- Свайп для удаления или кнопка корзины
- Пустое состояние: «Сохраните рецепты, которые вам понравились»
Конфигурация
Новые переменные в .env:
GEMINI_API_KEY=your-gemini-api-key
PEXELS_API_KEY=your-pexels-api-key
Обновить internal/config/config.go:
type Config struct {
// ...
GeminiAPIKey string `envconfig:"GEMINI_API_KEY" required:"true"`
PexelsAPIKey string `envconfig:"PEXELS_API_KEY" required:"true"`
}
Зависимости Go
go get github.com/google/generative-ai-go/genai@latest
Pexels — чистый HTTP (net/http), без SDK.