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

242 lines
8.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Итерация 2: Управление продуктами
**Цель:** дать пользователю возможность вести список продуктов — вручную или через автодополнение. Рекомендации становятся персонализированными: Gemini учитывает имеющиеся продукты.
**Зависимости:** Итерация 1 (рекомендации должны принимать список продуктов).
**Ориентир:** [Summary.md](./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 Миграция
```sql
-- 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 ингредиентами:
```json
[
{
"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 Поиск
```go
// 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 Миграция
```sql
-- 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**
```json
[{
"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):
```json
[{
"name": "Куриная грудка",
"quantity": 500,
"unit": "g",
"mapping_id": "uuid или null"
}]
```
---
## 2.3 Интеграция с рекомендациями
Обновить `GET /recommendations`: если у пользователя есть продукты — включать их в промпт.
```go
// Если продуктов нет — промпт без них (базовые рекомендации)
// Если продукты есть — добавить секцию в промпт:
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 над иконкой.