import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:nearledaily/controllers/search_controller/product_match.dart'; import '../../modules/search/grocery_item.dart'; class GrocerySearchController extends ChangeNotifier { // ─── Controllers & Focus ────────────────────────────────────────────────── final TextEditingController textController = TextEditingController(); final FocusNode focusNode = FocusNode(); // ─── Animation ──────────────────────────────────────────────────────────── late AnimationController cartBarController; late Animation cartBarSlide; // ─── State ──────────────────────────────────────────────────────────────── List items = []; List cartItems = []; bool loading = false; bool hasSearched = false; String lastQuery = ''; // ─── API ────────────────────────────────────────────────────────────────── static const String _apiKey = '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'; // ─── Quick Chips ────────────────────────────────────────────────────────── final List> quickChips = [ {'label': '🍛 Biryani', 'query': 'Biryani'}, {'label': '🍝 Pasta', 'query': 'Pasta'}, {'label': '🎂 Cake', 'query': 'Cake'}, {'label': '🥞 Pancakes', 'query': 'Pancakes'}, {'label': '🫘 Dal Tadka', 'query': 'Dal Tadka'}, {'label': '🥘 Idli Sambar', 'query': 'Idli Sambar'}, {'label': '🍕 Pizza', 'query': 'Pizza'}, {'label': '🍜 Noodles', 'query': 'Noodles'}, ]; final List> naturalChips = [ { 'label': '🤧 I have a cold — what should I eat?', 'query': 'I have a cold, what fruits or foods should I eat?' }, { 'label': '💪 Post-workout nutrition', 'query': 'Post-workout recovery foods and snacks' }, { 'label': '🍽️ Biryani + Curd Rice together', 'query': 'I want to make biryani and also curd rice' }, { 'label': '🍳 Quick egg breakfast', 'query': 'Quick breakfast with eggs and bread' }, ]; // ─── Init / Dispose ─────────────────────────────────────────────────────── void init(TickerProvider vsync) { // Register StoreMatchController if not already registered if (!Get.isRegistered()) { Get.put(StoreMatchController()); } initAnimations(vsync); } void initAnimations(TickerProvider vsync) { cartBarController = AnimationController( vsync: vsync, duration: const Duration(milliseconds: 300), ); cartBarSlide = Tween( begin: const Offset(0, 1), end: Offset.zero, ).animate(CurvedAnimation( parent: cartBarController, curve: Curves.easeOutCubic, )); } void disposeAll() { textController.dispose(); focusNode.dispose(); cartBarController.dispose(); } // ─── Search ─────────────────────────────────────────────────────────────── Future search(String query) async { final q = query.trim(); if (q.isEmpty) return; focusNode.unfocus(); loading = true; hasSearched = true; lastQuery = q; items = []; notifyListeners(); try { final res = await http.post( Uri.parse('https://api.groq.com/openai/v1/chat/completions'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $_apiKey', }, body: jsonEncode({ 'model': 'llama-3.1-8b-instant', 'max_tokens': 800, 'messages': [ { 'role': 'system', 'content': '''You are a smart product search engine for a super-app with grocery stores, restaurants, pharmacies, and general stores. Your ONLY job is to return a raw JSON array. No markdown, no code fences, no explanations. Raw JSON only. ═══════════════════════════════════════════ GOLDEN RULE — LITERAL FIRST: If the user types a RAW INGREDIENT, PRODUCT, or ITEM NAME (e.g. "tomato", "milk", "onion", "shampoo", "paracetamol"), return THAT EXACT ITEM as a grocery/store product. Do NOT transform it into a dish or recipe. ═══════════════════════════════════════════ INTENT DETECTION — pick ONE mode: MODE 1 — DIRECT PRODUCT SEARCH (user types a product/ingredient name): Signals: single word or short phrase that IS a product (e.g. "tomato", "milk", "bread", "eggs", "rice") Action: Return that exact product with 2–4 size/variant options only. Do NOT suggest dishes made from it. type: "grocery" Example — query "tomato": [ {"name": "Tomato", "quantity": "500g", "emoji": "🍅", "type": "grocery"}, {"name": "Tomato", "quantity": "1 kg", "emoji": "🍅", "type": "grocery"}, {"name": "Cherry Tomato", "quantity": "250g", "emoji": "🍅", "type": "grocery"} ] MODE 2 — RESTAURANT ORDER (user wants a ready-made dish delivered): Signals: dish name WITH food-order context, or words like "order", "hungry", "eat", "food delivery", "i want to eat [dish]", "get me [dish]" Action: Return the dish as 1–3 variants. type: "restaurant" Example — query "i want biryani": [ {"name": "Chicken Biryani", "quantity": "1 portion", "emoji": "🍛", "type": "restaurant"}, {"name": "Veg Biryani", "quantity": "1 portion", "emoji": "🍚", "type": "restaurant"} ] MODE 3 — COOK AT HOME (user wants to cook): Signals: "cook", "recipe", "ingredients for", "how to make", "groceries for", "i will make" Action: Return grocery ingredient list (6–12 items). type: "grocery" MODE 4 — SITUATION / NEED (health, mood, lifestyle): Signals: "sick", "fever", "diet", "weight loss", "party", "snacks", "breakfast", "workout" Action: Return 5–10 relevant products. Mix types if appropriate. ═══════════════════════════════════════════ STRICT ACCURACY RULES: - A product name is a product — never auto-upgrade it to a dish - Only return items the user actually needs — no upsells, no extras - If query is ambiguous between product vs dish, choose MODE 1 (product) - "tomato" → tomatoes, NOT tomato rice or tomato sauce - "chicken" → raw chicken cuts, NOT chicken curry or chicken burger - "lemon" → lemons, NOT lemon juice or lemon cake ═══════════════════════════════════════════ OUTPUT RULES: - Raw JSON array only. Nothing else. - Every item: exactly 4 keys — "name", "quantity", "emoji", "type" - "type": only "restaurant" or "grocery" - No duplicates. No nulls. No empty strings. - Max 6 items total unless MODE 3 (ingredients list).''', }, {'role': 'user', 'content': q}, ], }), ); if (res.statusCode != 200) { throw Exception('API error ${res.statusCode}: ${res.body}'); } final data = jsonDecode(res.body) as Map; final content = data['choices'][0]['message']['content'] as String; final start = content.indexOf('['); final end = content.lastIndexOf(']') + 1; if (start == -1 || end == 0) { throw Exception('No JSON array found in response'); } final List parsed = jsonDecode(content.substring(start, end)) as List; final newItems = parsed .asMap() .entries .map((e) => GroceryItem.fromJson(e.key, e.value as Map)) .toList(); // Preserve cart state for already-added items for (final item in newItems) { if (cartItems.any((c) => c.name == item.name)) { item.inCart = true; } } items = newItems; // ── Trigger store matching ────────────────────────────────────────── if (items.isNotEmpty) { try { final storeCtrl = Get.find(); await storeCtrl.matchAiItems(items); } catch (e, st) { print('[StoreMatch] Error during matching: $e'); print(' StackTrace: $st'); } } } on FormatException catch (e) { print('[GrocerySearch] FormatException: $e'); } on http.ClientException catch (e) { print('[GrocerySearch] Network error: $e'); } catch (e, st) { print('[GrocerySearch] Unexpected error: $e'); print(' StackTrace: $st'); } finally { loading = false; notifyListeners(); } } // ─── Cart ───────────────────────────────────────────────────────────────── void toggleCart(GroceryItem item) { HapticFeedback.lightImpact(); if (item.inCart) { item.inCart = false; cartItems.removeWhere((c) => c.id == item.id && c.name == item.name); } else { item.inCart = true; cartItems.add(item); } cartItems.isNotEmpty ? cartBarController.forward() : cartBarController.reverse(); notifyListeners(); } void addAllToCart() { HapticFeedback.mediumImpact(); for (final item in items) { if (!item.inCart) { item.inCart = true; cartItems.add(item); } } cartBarController.forward(); notifyListeners(); } bool get allInCart => items.isNotEmpty && items.every((i) => i.inCart); void removeFromCart(GroceryItem item) { item.inCart = false; cartItems.removeWhere((c) => c.id == item.id && c.name == item.name); if (cartItems.isEmpty) cartBarController.reverse(); notifyListeners(); } // ─── Helpers ────────────────────────────────────────────────────────────── void clearSearch() { textController.clear(); hasSearched = false; items = []; lastQuery = ''; focusNode.requestFocus(); notifyListeners(); } void fillSearch(String query) { textController.text = query; search(query); } }