import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; // ───────────────────────────────────────────── // CONFIG — change IP to your Mac's local IP // Run: ipconfig getifaddr en0 // ───────────────────────────────────────────── const String kBaseUrl = 'http://192.168.1.XXX:8000'; const int kCustomerId = 6060; const double kLatitude = 11.0168; const double kLongitude = 76.9558; // ───────────────────────────────────────────── // API SERVICE // ───────────────────────────────────────────── class AgentService { static Future> findIngredients(String userInput) async { final res = await http.post( Uri.parse('$kBaseUrl/find-ingredients'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'user_input': userInput, 'customer_id': kCustomerId, 'latitude': kLatitude, 'longitude': kLongitude, }), ); return jsonDecode(res.body); } static Future> chat( String message, List conversationHistory, ) async { final res = await http.post( Uri.parse('$kBaseUrl/chat'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'message': message, 'customer_id': kCustomerId, 'latitude': kLatitude, 'longitude': kLongitude, 'conversation_history': conversationHistory, }), ); return jsonDecode(res.body); } static Future> clarify( String originalInput, String clarification, ) async { final res = await http.post( Uri.parse('$kBaseUrl/clarify'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'original_input': originalInput, 'clarification': clarification, 'customer_id': kCustomerId, 'latitude': kLatitude, 'longitude': kLongitude, }), ); return jsonDecode(res.body); } } // ───────────────────────────────────────────── // SEARCH SCREEN // ───────────────────────────────────────────── class SearchScreen extends StatefulWidget { const SearchScreen({super.key}); @override State createState() => _SearchScreenState(); } class _SearchScreenState extends State { final TextEditingController _searchController = TextEditingController(); final FocusNode _focusNode = FocusNode(); bool _isLoading = false; String? _agentMessage; String? _clarificationQuestion; String? _lastInput; List _ingredients = []; List _stores = []; // Quick suggestion chips final List _suggestions = [ '🍓 Strawberry', '🍗 Biryani', '🤒 Fever medicine', '🍳 Cook sambar', '🥛 Milk', '😴 Bored snacks', ]; // ── Search via /find-ingredients ────────────────────────── Future _search(String input) async { if (input.trim().isEmpty) return; FocusScope.of(context).unfocus(); setState(() { _isLoading = true; _agentMessage = null; _clarificationQuestion = null; _stores = []; _ingredients = []; _lastInput = input; }); try { final result = await AgentService.findIngredients(input); if (result['status'] == 'needs_clarification') { setState(() { _clarificationQuestion = result['question']; _isLoading = false; }); return; } setState(() { _agentMessage = result['agent_message']; _ingredients = result['ingredients_needed'] ?? []; _stores = result['stores'] ?? []; _isLoading = false; }); } catch (e) { setState(() { _agentMessage = 'Something went wrong. Please try again.'; _isLoading = false; }); } } // ── Answer agent clarification ──────────────────────────── Future _answerClarification(String answer) async { setState(() { _clarificationQuestion = null; _isLoading = true; }); try { final result = await AgentService.clarify(_lastInput ?? '', answer); setState(() { _agentMessage = result['agent_message']; _ingredients = result['ingredients_needed'] ?? []; _stores = result['stores'] ?? []; _isLoading = false; }); } catch (e) { setState(() { _agentMessage = 'Something went wrong.'; _isLoading = false; }); } } // ───────────────────────────────────────────────────────── // BUILD // ───────────────────────────────────────────────────────── @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: AppBar( backgroundColor: Colors.white, elevation: 0, title: const Text( 'Nearle', style: TextStyle( color: Color(0xFF1A1A1A), fontWeight: FontWeight.bold, fontSize: 20, ), ), centerTitle: false, ), body: Column( children: [ // ── Search Bar ────────────────────────────────── Container( color: Colors.white, padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Row( children: [ Expanded( child: TextField( controller: _searchController, focusNode: _focusNode, onSubmitted: _search, decoration: InputDecoration( hintText: 'Search or ask anything...', hintStyle: TextStyle(color: Colors.grey[400]), prefixIcon: const Icon(Icons.search, color: Color(0xFF6C63FF)), filled: true, fillColor: const Color(0xFFF0EFFF), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric(vertical: 14), ), ), ), const SizedBox(width: 10), GestureDetector( onTap: () => _search(_searchController.text), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFF6C63FF), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.arrow_forward, color: Colors.white, size: 20), ), ), ], ), ), // ── Suggestion Chips ──────────────────────────── if (_stores.isEmpty && !_isLoading && _clarificationQuestion == null) SizedBox( height: 48, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _suggestions.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, i) { return GestureDetector( onTap: () { final text = _suggestions[i].replaceAll(RegExp(r'[^\w\s]'), '').trim(); _searchController.text = text; _search(text); }, child: Chip( label: Text( _suggestions[i], style: const TextStyle(fontSize: 13), ), backgroundColor: Colors.white, side: const BorderSide(color: Color(0xFFE0E0E0)), ), ); }, ), ), const SizedBox(height: 8), // ── Body ──────────────────────────────────────── Expanded( child: _isLoading ? const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: Color(0xFF6C63FF)), SizedBox(height: 16), Text('Anna is finding the best deals...', style: TextStyle(color: Colors.grey)), ], ), ) : _clarificationQuestion != null ? _buildClarification() : _stores.isEmpty ? _buildEmpty() : _buildResults(), ), ], ), ); } // ── Clarification UI ────────────────────────────────────── Widget _buildClarification() { final answerController = TextEditingController(); return Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFEDE9FF), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ const Text('🤔', style: TextStyle(fontSize: 24)), const SizedBox(width: 12), Expanded( child: Text( _clarificationQuestion!, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), ), ), ], ), ), const SizedBox(height: 16), TextField( controller: answerController, decoration: InputDecoration( hintText: 'Type your answer...', filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), ), onSubmitted: _answerClarification, ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => _answerClarification(answerController.text), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C63FF), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: const Text('Send', style: TextStyle(color: Colors.white, fontSize: 16)), ), ), ], ), ); } // ── Empty State ─────────────────────────────────────────── Widget _buildEmpty() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('🛒', style: TextStyle(fontSize: 60)), const SizedBox(height: 16), Text( _agentMessage ?? 'Search for anything\nfrom nearby stores', textAlign: TextAlign.center, style: const TextStyle(fontSize: 16, color: Colors.grey), ), ], ), ); } // ── Results ─────────────────────────────────────────────── Widget _buildResults() { return ListView( padding: const EdgeInsets.all(16), children: [ // Agent message if (_agentMessage != null) Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFEDE9FF), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ const Text('✨', style: TextStyle(fontSize: 20)), const SizedBox(width: 10), Expanded(child: Text(_agentMessage!, style: const TextStyle(fontSize: 14))), ], ), ), // Store cards ..._stores.map((storeData) => _StoreCard(storeData: storeData)), ], ); } } // ───────────────────────────────────────────── // STORE CARD WIDGET // ───────────────────────────────────────────── class _StoreCard extends StatelessWidget { final Map storeData; const _StoreCard({required this.storeData}); @override Widget build(BuildContext context) { final store = storeData['store'] as Map; final products = storeData['matched_products'] as List; final savings = store['total_savings'] ?? 0; final total = store['total_discounted_price'] ?? 0; final hasOffers = store['has_offers'] == true; return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Store header Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: const Color(0xFFF0EFFF), borderRadius: BorderRadius.circular(10), ), child: const Icon(Icons.store, color: Color(0xFF6C63FF), size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( store['tenantname'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), Text( store['locationname'] ?? '', style: const TextStyle(color: Colors.grey, fontSize: 13), ), ], ), ), if (hasOffers) Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: const Color(0xFFE8F5E9), borderRadius: BorderRadius.circular(20), ), child: Text( 'Save ₹${savings.toStringAsFixed(0)}', style: const TextStyle(color: Color(0xFF2E7D32), fontWeight: FontWeight.bold, fontSize: 12), ), ), ], ), ), const Divider(height: 1), // Products ...products.map((m) => _ProductRow(matched: m)), const Divider(height: 1), // Total + Order button Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Total', style: TextStyle(color: Colors.grey, fontSize: 12)), Text( '₹${total.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Color(0xFF1A1A1A)), ), ], ), const Spacer(), ElevatedButton( onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C63FF), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), child: const Text('Order Now', style: TextStyle(color: Colors.white)), ), ], ), ), ], ), ); } } // ───────────────────────────────────────────── // PRODUCT ROW WIDGET // ───────────────────────────────────────────── class _ProductRow extends StatelessWidget { final Map matched; const _ProductRow({required this.matched}); @override Widget build(BuildContext context) { final product = matched['product'] as Map; final emoji = matched['emoji'] ?? '🛒'; final name = product['productname'] ?? matched['ingredient'] ?? ''; final price = product['discounted_price'] ?? 0; final original = product['original_price'] ?? 0; final hasOffer = product['has_offer'] == true; final discount = product['discount_percent'] ?? 0; final quantity = matched['quantity'] ?? ''; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row( children: [ Text(emoji, style: const TextStyle(fontSize: 24)), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), Text(quantity, style: const TextStyle(color: Colors.grey, fontSize: 12)), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '₹${price.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Color(0xFF1A1A1A)), ), if (hasOffer) Row( children: [ Text( '₹${original.toStringAsFixed(0)}', style: const TextStyle( color: Colors.grey, fontSize: 11, decoration: TextDecoration.lineThrough, ), ), const SizedBox(width: 4), Text( '$discount% off', style: const TextStyle(color: Color(0xFF2E7D32), fontSize: 11, fontWeight: FontWeight.bold), ), ], ), ], ), ], ), ); } }