import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import '../../core/api/api_client.dart'; import '../../core/auth/auth_provider.dart'; // --------------------------------------------------------------------------- // Models // --------------------------------------------------------------------------- class RecognizedItem { final String name; double quantity; String unit; final String category; final double confidence; final String? mappingId; final int storageDays; RecognizedItem({ required this.name, required this.quantity, required this.unit, required this.category, required this.confidence, this.mappingId, required this.storageDays, }); factory RecognizedItem.fromJson(Map json) { return RecognizedItem( name: json['name'] as String? ?? '', quantity: (json['quantity'] as num?)?.toDouble() ?? 1.0, unit: json['unit'] as String? ?? 'шт', category: json['category'] as String? ?? 'other', confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0, mappingId: json['mapping_id'] as String?, storageDays: json['storage_days'] as int? ?? 7, ); } } class UnrecognizedItem { final String rawText; final double? price; const UnrecognizedItem({required this.rawText, this.price}); factory UnrecognizedItem.fromJson(Map json) { return UnrecognizedItem( rawText: json['raw_text'] as String? ?? '', price: (json['price'] as num?)?.toDouble(), ); } } class ReceiptResult { final List items; final List unrecognized; const ReceiptResult({required this.items, required this.unrecognized}); } /// A single dish recognition candidate with estimated nutrition for the portion in the photo. class DishCandidate { final String? dishId; final String dishName; final int weightGrams; final double calories; final double proteinG; final double fatG; final double carbsG; final double confidence; const DishCandidate({ this.dishId, required this.dishName, required this.weightGrams, required this.calories, required this.proteinG, required this.fatG, required this.carbsG, required this.confidence, }); factory DishCandidate.fromJson(Map json) { return DishCandidate( dishId: json['dish_id'] as String?, dishName: json['dish_name'] as String? ?? '', weightGrams: json['weight_grams'] as int? ?? 0, calories: (json['calories'] as num?)?.toDouble() ?? 0, proteinG: (json['protein_g'] as num?)?.toDouble() ?? 0, fatG: (json['fat_g'] as num?)?.toDouble() ?? 0, carbsG: (json['carbs_g'] as num?)?.toDouble() ?? 0, confidence: (json['confidence'] as num?)?.toDouble() ?? 0, ); } } /// Result of dish recognition: ordered list of candidates (best match first). class DishResult { final List candidates; const DishResult({required this.candidates}); /// The best matching candidate. DishCandidate get best => candidates.first; // Convenience getters delegating to the best candidate. String get dishName => best.dishName; int get weightGrams => best.weightGrams; double get calories => best.calories; double get proteinG => best.proteinG; double get fatG => best.fatG; double get carbsG => best.carbsG; double get confidence => best.confidence; factory DishResult.fromJson(Map json) { // New format: {"candidates": [...]} if (json['candidates'] is List) { final candidatesList = (json['candidates'] as List) .map((element) => DishCandidate.fromJson(element as Map)) .toList(); return DishResult(candidates: candidatesList); } // Legacy flat format: {"dish_name": "...", "calories": ..., ...} if (json['dish_name'] != null) { return DishResult(candidates: [DishCandidate.fromJson(json)]); } return const DishResult(candidates: []); } } // --------------------------------------------------------------------------- // Service // --------------------------------------------------------------------------- class RecognitionService { const RecognitionService(this._client); final ApiClient _client; /// Recognizes food items from a receipt photo. Future recognizeReceipt(XFile image) async { final payload = await _buildImagePayload(image); final data = await _client.post('/ai/recognize-receipt', data: payload); return ReceiptResult( items: (data['items'] as List? ?? []) .map((e) => RecognizedItem.fromJson(e as Map)) .toList(), unrecognized: (data['unrecognized'] as List? ?? []) .map((e) => UnrecognizedItem.fromJson(e as Map)) .toList(), ); } /// Recognizes food items from 1–3 product photos. Future> recognizeProducts(List images) async { final imageList = await Future.wait(images.map(_buildImagePayload)); final data = await _client.post( '/ai/recognize-products', data: {'images': imageList}, ); return (data['items'] as List? ?? []) .map((e) => RecognizedItem.fromJson(e as Map)) .toList(); } /// Recognizes a dish and estimates its nutritional content. Future recognizeDish(XFile image) async { final payload = await _buildImagePayload(image); final data = await _client.post('/ai/recognize-dish', data: payload); return DishResult.fromJson(data); } Future> _buildImagePayload(XFile image) async { final bytes = await image.readAsBytes(); final base64Data = base64Encode(bytes); // XFile.mimeType may be null on some platforms; fall back to path extension. final mimeType = image.mimeType ?? (image.path.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg'); return {'image_base64': base64Data, 'mime_type': mimeType}; } } final recognitionServiceProvider = Provider((ref) { return RecognitionService(ref.read(apiClientProvider)); });