Files
food-ai/client/lib/features/scan/dish_result_screen.dart
dbastrikin deceedd4a7 feat: implement Iteration 3 — product/receipt/dish recognition
Backend:
- gemini/client.go: refactor to shared callGroq transport; add
  generateVisionContent using llama-3.2-11b-vision-preview model
- gemini/recognition.go: RecognizeReceipt, RecognizeProducts,
  RecognizeDish (vision), ClassifyIngredient (text); shared parseJSON helper
- ingredient/repository.go: add FuzzyMatch (wraps Search, returns best hit)
- recognition/handler.go: POST /ai/recognize-receipt, /ai/recognize-products,
  /ai/recognize-dish; enrichItems with fuzzy match + AI classify fallback;
  parallel multi-image processing with deduplication
- server.go + main.go: wire recognition handler under /ai routes

Flutter:
- pubspec.yaml: add image_picker ^1.1.0
- AndroidManifest.xml: add CAMERA and READ_EXTERNAL_STORAGE permissions
- Info.plist: add NSCameraUsageDescription and NSPhotoLibraryUsageDescription
- recognition_service.dart: RecognitionService wrapping /ai/* endpoints;
  RecognizedItem, ReceiptResult, DishResult models
- scan_screen.dart: mode selector (receipt / products / dish / manual);
  image source picker; loading overlay; navigates to confirm or dish screen
- recognition_confirm_screen.dart: editable list of recognized items;
  inline qty/unit editing; swipe-to-delete; batch-add to pantry
- dish_result_screen.dart: dish name, KBZHU breakdown, similar dishes chips
- app_router.dart: /scan, /scan/confirm, /scan/dish routes (no bottom nav)
- products_screen.dart: FAB now shows bottom sheet with Manual / Scan options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 10:54:03 +02:00

168 lines
5.1 KiB
Dart
Raw 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.
import 'package:flutter/material.dart';
import 'recognition_service.dart';
/// Shows the nutritional breakdown of a recognized dish.
class DishResultScreen extends StatelessWidget {
const DishResultScreen({super.key, required this.dish});
final DishResult dish;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final confPct = (dish.confidence * 100).toInt();
return Scaffold(
appBar: AppBar(title: const Text('Распознано блюдо')),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
// Dish name + confidence
Text(
dish.dishName,
style: theme.textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.info_outline,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'Уверенность: $confPct%',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 24),
// Nutrition card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'${dish.calories.toInt()} ккал',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const Spacer(),
Tooltip(
message: 'Приблизительные значения на основе фото',
child: Text(
'',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_MacroChip(
label: 'Белки',
value: '${dish.proteinG.toStringAsFixed(1)} г',
color: Colors.blue,
),
_MacroChip(
label: 'Жиры',
value: '${dish.fatG.toStringAsFixed(1)} г',
color: Colors.orange,
),
_MacroChip(
label: 'Углеводы',
value: '${dish.carbsG.toStringAsFixed(1)} г',
color: Colors.green,
),
],
),
const SizedBox(height: 8),
Text(
'Вес порции: ~${dish.weightGrams} г',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
// Similar dishes
if (dish.similarDishes.isNotEmpty) ...[
const SizedBox(height: 20),
Text('Похожие блюда', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 4,
children: dish.similarDishes
.map((name) => Chip(label: Text(name)))
.toList(),
),
],
const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 8),
Text(
'КБЖУ приблизительные — определены по фото.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
);
}
}
class _MacroChip extends StatelessWidget {
const _MacroChip({
required this.label,
required this.value,
required this.color,
});
final String label;
final String value;
final Color color;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: color,
),
),
Text(
label,
style: Theme.of(context).textTheme.labelSmall,
),
],
);
}
}