Update project

This commit is contained in:
2026-07-23 15:36:11 +05:30
parent 1197cfc161
commit 14fffa40c6
84 changed files with 20918 additions and 2761 deletions

View File

@@ -0,0 +1,295 @@
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<Offset> cartBarSlide;
// ─── State ────────────────────────────────────────────────────────────────
List<GroceryItem> items = [];
List<GroceryItem> cartItems = [];
bool loading = false;
bool hasSearched = false;
String lastQuery = '';
// ─── API ──────────────────────────────────────────────────────────────────
static const String _apiKey =
'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';
// ─── Quick Chips ──────────────────────────────────────────────────────────
final List<Map<String, String>> 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<Map<String, String>> 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<StoreMatchController>()) {
Get.put(StoreMatchController());
}
initAnimations(vsync);
}
void initAnimations(TickerProvider vsync) {
cartBarController = AnimationController(
vsync: vsync,
duration: const Duration(milliseconds: 300),
);
cartBarSlide = Tween<Offset>(
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<void> 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 24 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 13 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 (612 items).
type: "grocery"
MODE 4 — SITUATION / NEED (health, mood, lifestyle):
Signals: "sick", "fever", "diet", "weight loss", "party", "snacks", "breakfast", "workout"
Action: Return 510 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<String, dynamic>;
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<String, dynamic>))
.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<StoreMatchController>();
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);
}
}