Files
food-ai/docs/plans/Iteration_2.md
dbastrikin e57ff8e06c feat: implement Iteration 1 — AI recipe recommendations
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>
2026-02-21 22:43:29 +02:00

8.6 KiB
Raw Blame History

Итерация 2: Управление продуктами

Цель: дать пользователю возможность вести список продуктов — вручную или через автодополнение. Рекомендации становятся персонализированными: Gemini учитывает имеющиеся продукты.

Зависимости: Итерация 1 (рекомендации должны принимать список продуктов).

Ориентир: Summary.md


Структура задач

2.1 Backend: ingredient_mappings
 ├── 2.1.1 Миграция: таблица ingredient_mappings
 ├── 2.1.2 Seed: топ-200 базовых ингредиентов (JSON-файл)
 ├── 2.1.3 Repository (поиск по aliases)
 └── 2.1.4 GET /ingredients/search?q=

2.2 Backend: products
 ├── 2.2.1 Миграция: таблица products
 ├── 2.2.2 Repository (CRUD)
 ├── 2.2.3 Service layer
 ├── 2.2.4 GET /products
 ├── 2.2.5 POST /products
 ├── 2.2.6 POST /products/batch
 ├── 2.2.7 PUT /products/{id}
 ├── 2.2.8 DELETE /products/{id}
 └── 2.2.9 GET /products/expiring (скоро истекают)

2.3 Backend: интеграция с рекомендациями
 └── 2.3.1 GET /recommendations — добавить продукты в промпт

2.4 Flutter: экран продуктов
 ├── 2.4.1 ProductsScreen (список с истекающими)
 ├── 2.4.2 Форма добавления с автодополнением (debounce 300мс)
 ├── 2.4.3 Редактирование (количество, единица, срок)
 └── 2.4.4 Удаление (свайп или кнопка)

2.1 Ingredient Mappings

2.1.1 Миграция

-- migrations/003_create_ingredient_mappings.sql

-- +goose Up
CREATE TABLE ingredient_mappings (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    canonical_name      TEXT NOT NULL UNIQUE,
    canonical_name_ru   TEXT NOT NULL,
    aliases             JSONB NOT NULL DEFAULT '[]',
    category            TEXT NOT NULL,
    default_unit        TEXT NOT NULL DEFAULT 'g',
    calories_per_100g   DECIMAL(8,2),
    protein_per_100g    DECIMAL(8,2),
    fat_per_100g        DECIMAL(8,2),
    carbs_per_100g      DECIMAL(8,2),
    storage_days        INT NOT NULL DEFAULT 7,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_ingredient_mappings_aliases ON ingredient_mappings USING GIN(aliases);
CREATE INDEX idx_ingredient_mappings_canonical_ru ON ingredient_mappings
    USING GIN(to_tsvector('russian', canonical_name_ru));
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_ingredient_mappings_trgm ON ingredient_mappings
    USING GIN(canonical_name_ru gin_trgm_ops);

-- +goose Down
DROP TABLE ingredient_mappings;

2.1.2 Seed-данные

Файл migrations/seed_ingredient_mappings.json с топ-200 ингредиентами:

[
  {
    "canonical_name": "chicken_breast",
    "canonical_name_ru": "куриная грудка",
    "aliases": ["куриное филе", "куриная грудка", "грудка курицы", "chicken breast"],
    "category": "meat",
    "default_unit": "g",
    "calories_per_100g": 165,
    "protein_per_100g": 31,
    "fat_per_100g": 3.6,
    "carbs_per_100g": 0,
    "storage_days": 3
  },
  ...
]

Seed применяется отдельным скриптом/Makefile-таргетом make seed.

2.1.3 Поиск

// GET /ingredients/search?q=кур&limit=10
// Трёхуровневый поиск:
// 1. Точное совпадение в aliases (@>)
// 2. ILIKE на canonical_name_ru
// 3. pg_trgm similarity > 0.3
SELECT *
FROM ingredient_mappings
WHERE aliases @> to_jsonb(lower($1)::text)
   OR canonical_name_ru ILIKE '%' || $1 || '%'
   OR similarity(canonical_name_ru, $1) > 0.3
ORDER BY similarity(canonical_name_ru, $1) DESC
LIMIT $2

2.2 Products

2.2.1 Миграция

-- migrations/004_create_products.sql

-- +goose Up
CREATE TABLE products (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    mapping_id      UUID REFERENCES ingredient_mappings(id),
    name            TEXT NOT NULL,
    quantity        DECIMAL(10,2) NOT NULL DEFAULT 1,
    unit            TEXT NOT NULL DEFAULT 'pcs',
    category        TEXT,
    storage_days    INT NOT NULL DEFAULT 7,
    added_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
    expires_at      TIMESTAMPTZ GENERATED ALWAYS AS
                    (added_at + (storage_days || ' days')::INTERVAL) STORED
);

CREATE INDEX idx_products_user_id ON products(user_id);
CREATE INDEX idx_products_expires_at ON products(user_id, expires_at);

-- +goose Down
DROP TABLE products;

2.2.2 Эндпоинты

GET /products

[{
  "id": "uuid",
  "name": "Куриная грудка",
  "quantity": 500,
  "unit": "g",
  "category": "meat",
  "expires_at": "2026-02-24T00:00:00Z",
  "days_left": 3,
  "expiring_soon": true
}]

Сортировка: expires_at ASC (сначала истекающие). expiring_soon = true если days_left <= 3.

POST /products/batch

Массовое добавление после распознавания (Итерация 3):

[{
  "name": "Куриная грудка",
  "quantity": 500,
  "unit": "g",
  "mapping_id": "uuid или null"
}]

2.3 Интеграция с рекомендациями

Обновить GET /recommendations: если у пользователя есть продукты — включать их в промпт.

// Если продуктов нет — промпт без них (базовые рекомендации)
// Если продукты есть — добавить секцию в промпт:

doступные продукты (приоритет  скоро истекают ):
- Куриная грудка 500г (истекает завтра )
- Морковь 3 шт
- Рис 400г
- Яйца 4 шт

Предпочтительно использовать эти продукты в рецептах.

2.4 Flutter: экран продуктов

ProductsScreen

┌─────────────────────────────────────┐
│ Мои продукты              [+ Добавить] │
├─────────────────────────────────────┤
│ ⚠ Истекает скоро                   │
│  ┌──────────────────────────────┐   │
│  │ 🥩 Куриная грудка   500 г   │   │
│  │    Осталось 1 день          │   │
│  └──────────────────────────────┘   │
│                                     │
│ Всё остальное                       │
│  ┌──────────────────────────────┐   │
│  │ 🥕 Морковь          3 шт    │   │
│  │    Осталось 5 дней          │   │
│  └──────────────────────────────┘   │
│  ┌──────────────────────────────┐   │
│  │ 🍚 Рис            400 г     │   │
│  │    Осталось 30 дней         │   │
│  └──────────────────────────────┘   │
├─────────────────────────────────────┤
│ [Главная] [Продукты●] [Меню] [Рецепты] [Профиль] │
└─────────────────────────────────────┘

Форма добавления

  • Поле ввода с debounce 300мсGET /ingredients/search?q=
  • Dropdown с результатами (canonical_name_ru + category)
  • При выборе: автозаполнение unit, storage_days из mapping
  • Поля: Количество, Единица (select: г/кг/мл/л/шт)
  • Кнопка «Добавить»

Badge на вкладке «Продукты»

Количество продуктов с days_left <= 3 отображается как badge над иконкой.