feat: meal tracking, dish recognition UX improvements, English AI prompts

Backend:
- Translate all recognition prompts (receipt, products, dish) from Russian to English
- Add lang parameter to Recognizer interface and pass locale.FromContext in handlers
- DishResult type uses candidates array for multi-candidate responses

Client:
- Add meal tracking: diary provider, date selector, meal type model
- DishResult parser: backward-compatible with legacy flat format and new candidates format
- DishResultScreen: sticky bottom button, full-width portion/meal-type inputs,
  КБЖУ disclaimer moved under nutrition card, add date field to diary POST body
- Recognition prompts now return dish/product names in user's preferred language
- Onboarding, profile, home screen visual updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-17 14:29:36 +02:00
parent 2a95bcd53c
commit 87ef2097fc
16 changed files with 1269 additions and 350 deletions

View File

@@ -9,6 +9,7 @@ import (
"sync"
"github.com/food-ai/backend/internal/adapters/ai"
"github.com/food-ai/backend/internal/infra/locale"
"github.com/food-ai/backend/internal/infra/middleware"
"github.com/food-ai/backend/internal/domain/ingredient"
)
@@ -23,9 +24,9 @@ type IngredientRepository interface {
// Recognizer is the AI provider interface for image-based food recognition.
type Recognizer interface {
RecognizeReceipt(ctx context.Context, imageBase64, mimeType string) (*ai.ReceiptResult, error)
RecognizeProducts(ctx context.Context, imageBase64, mimeType string) ([]ai.RecognizedItem, error)
RecognizeDish(ctx context.Context, imageBase64, mimeType string) (*ai.DishResult, error)
RecognizeReceipt(ctx context.Context, imageBase64, mimeType, lang string) (*ai.ReceiptResult, error)
RecognizeProducts(ctx context.Context, imageBase64, mimeType, lang string) ([]ai.RecognizedItem, error)
RecognizeDish(ctx context.Context, imageBase64, mimeType, lang string) (*ai.DishResult, error)
ClassifyIngredient(ctx context.Context, name string) (*ai.IngredientClassification, error)
}
@@ -91,7 +92,8 @@ func (h *Handler) RecognizeReceipt(w http.ResponseWriter, r *http.Request) {
return
}
result, err := h.recognizer.RecognizeReceipt(r.Context(), req.ImageBase64, req.MimeType)
lang := locale.FromContext(r.Context())
result, err := h.recognizer.RecognizeReceipt(r.Context(), req.ImageBase64, req.MimeType, lang)
if err != nil {
slog.Error("recognize receipt", "err", err)
writeErrorJSON(w, http.StatusServiceUnavailable, "recognition failed, please try again")
@@ -118,13 +120,14 @@ func (h *Handler) RecognizeProducts(w http.ResponseWriter, r *http.Request) {
}
// Process each image in parallel.
lang := locale.FromContext(r.Context())
allItems := make([][]ai.RecognizedItem, len(req.Images))
var wg sync.WaitGroup
for i, img := range req.Images {
wg.Add(1)
go func(i int, img imageRequest) {
defer wg.Done()
items, err := h.recognizer.RecognizeProducts(r.Context(), img.ImageBase64, img.MimeType)
items, err := h.recognizer.RecognizeProducts(r.Context(), img.ImageBase64, img.MimeType, lang)
if err != nil {
slog.Warn("recognize products from image", "index", i, "err", err)
return
@@ -148,7 +151,8 @@ func (h *Handler) RecognizeDish(w http.ResponseWriter, r *http.Request) {
return
}
result, err := h.recognizer.RecognizeDish(r.Context(), req.ImageBase64, req.MimeType)
lang := locale.FromContext(r.Context())
result, err := h.recognizer.RecognizeDish(r.Context(), req.ImageBase64, req.MimeType, lang)
if err != nil {
slog.Error("recognize dish", "err", err)
writeErrorJSON(w, http.StatusServiceUnavailable, "recognition failed, please try again")

View File

@@ -112,7 +112,7 @@ func buildUpdateQuery(id string, req UpdateProfileRequest) (string, []interface{
argIdx++
}
if req.Preferences != nil {
setClauses = append(setClauses, fmt.Sprintf("preferences = $%d", argIdx))
setClauses = append(setClauses, fmt.Sprintf("preferences = preferences || $%d::jsonb", argIdx))
args = append(args, string(*req.Preferences))
argIdx++
}