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

@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/auth/auth_provider.dart';
import '../../core/locale/language_provider.dart';
import '../../core/theme/app_colors.dart';
import '../../shared/models/meal_type.dart';
import '../../shared/models/user.dart';
import 'profile_provider.dart';
import 'profile_service.dart';
@@ -92,7 +93,7 @@ class _ProfileBody extends ConsumerWidget {
]),
const SizedBox(height: 16),
// Calories
// Calories + meal types
_SectionLabel('ПИТАНИЕ'),
const SizedBox(height: 6),
_InfoCard(children: [
@@ -102,6 +103,14 @@ class _ProfileBody extends ConsumerWidget {
? '${user.dailyCalories} ккал/день'
: null,
),
const Divider(height: 1, indent: 16),
_InfoRow(
'Приёмы пищи',
user.mealTypes
.map((mealTypeId) =>
mealTypeById(mealTypeId)?.label ?? mealTypeId)
.join(', '),
),
]),
if (user.dailyCalories != null) ...[
const SizedBox(height: 4),
@@ -342,6 +351,7 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
String? _goal;
String? _activity;
String? _language;
List<String> _mealTypes = [];
bool _saving = false;
@override
@@ -358,6 +368,7 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
_goal = u.goal;
_activity = u.activity;
_language = u.preferences['language'] as String? ?? 'ru';
_mealTypes = List<String>.from(u.mealTypes);
}
@override
@@ -400,6 +411,7 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
goal: _goal,
activity: _activity,
language: _language,
mealTypes: _mealTypes,
);
final ok = await ref.read(profileProvider.notifier).update(req);
@@ -590,12 +602,39 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
),
const SizedBox(height: 20),
// Meal types
Text('Приёмы пищи', style: theme.textTheme.labelMedium),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 6,
children: kAllMealTypes.map((mealTypeOption) {
final isSelected =
_mealTypes.contains(mealTypeOption.id);
return FilterChip(
label: Text(
'${mealTypeOption.emoji} ${mealTypeOption.label}'),
selected: isSelected,
onSelected: (selected) {
setState(() {
if (selected) {
_mealTypes.add(mealTypeOption.id);
} else if (_mealTypes.length > 1) {
_mealTypes.remove(mealTypeOption.id);
}
});
},
);
}).toList(),
),
const SizedBox(height: 20),
// Language
ref.watch(supportedLanguagesProvider).when(
data: (languages) => DropdownButtonFormField<String>(
value: _language,
decoration: const InputDecoration(
labelText: 'Язык интерфейса'),
initialValue: _language,
items: languages.entries
.map((e) => DropdownMenuItem(
value: e.key,