import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../core/constants/app_constants.dart'; import '../../core/theme/app_theme.dart'; import '../../providers/amor_chat_provider.dart'; import '../../services/gemini_service.dart'; // ── Screen ───────────────────────────────────────────────────────────────── class AmorChatbotScreen extends ConsumerStatefulWidget { const AmorChatbotScreen({super.key}); @override ConsumerState createState() => _AmorChatbotScreenState(); } class _AmorChatbotScreenState extends ConsumerState { final TextEditingController _chatController = TextEditingController(); final ScrollController _scrollCtrl = ScrollController(); // ── Full Amor Coffee Menu ───────────────────────────────────────────────── static const String _amorMenu = ''' MENU AMOR COFFEE (harga dalam Rupiah): SIGNATURE - Teh Sereh Lemon : Rp 10.000 - StrawCano : Rp 13.000 - Es Kopi Susu Amor : Rp 13.000 - Avocalypse : Rp 15.000 MANUAL BREW - V60/Kopi Filter : Rp 15.000 - Kopi Tubruk : Rp 7.000 COFFEE - Americano : Rp 10.000 - Aren Latte : Rp 13.000 - Caramel Latte : Rp 14.000 - Hazelnut Latte : Rp 14.000 - Vanilla Latte : Rp 14.000 - Cappucino (Hot) : Rp 14.000 - ButterScotch Latte : Rp 18.000 SQUASH - Strawberry : Rp 12.000 - Melon : Rp 12.000 - Grape : Rp 12.000 - Lemon : Rp 13.000 - Lychee : Rp 12.000 TEA - Original Tea : Rp 7.000 - Strawberry Tea : Rp 10.000 - Lychee Tea : Rp 10.000 - Lemon Tea : Rp 10.000 - Thai Tea : Rp 12.000 MILK BASED - Avocado : Rp 12.000 - Taro : Rp 12.000 - Redvelvet : Rp 12.000 - Pink Lava : Rp 12.000 - Chocolate : Rp 12.000 - Choco Caramel : Rp 13.000 - Choco Hazelnut : Rp 13.000 - Matcha : Rp 12.000 - Matcha Coconut : Rp 14.000 OTHERS (Minuman) - Extra Shot Espresso : Rp 4.000 - Mineral : Rp 5.000 FOOD - Nasilor (Nasi Sosis Telor) : Rp 10.000 - Chicken Katsu Mentai : Rp 12.000 - Chicken Katsu Blackpapper : Rp 12.000 - Chicken Katsu Spicy : Rp 12.000 - Indomie Kuah Amor (Indomie Kuah Topping Telur, Bakso, Sosis) : Rp 12.000 - Indomie Goreng Amor (Indomie Goreng Topping Telur, Mozzarella, Bakso) : Rp 15.000 SNACK - Donat Amor (choco/matcha/tiramisu) : Rp 10.000 - French Fries : Rp 10.000 - Ropang manis (choco/matcha/tiramisu): Rp 10.000 - Ropang Telur : Rp 11.000 - Ropang Beef : Rp 13.000 - Pisang Choco : Rp 12.000 - Tempe Mendoan : Rp 12.000 - Gyoza : Rp 13.000 - Dimsum Ayam : Rp 13.000 - Dimsum Mentai : Rp 15.000 - Mix Sampler : Rp 15.000 '''; final List _suggestions = [ 'Menu apa yang paling populer? 🔥', 'Ada menu di bawah Rp 12.000? 💰', 'Rekomendasiin minuman dingin dong 🧊', 'Paket makan + minum hemat? 🎁', 'Apa saja menu makanan berat? 🍽️', 'Perbedaan Matcha & Matcha Coconut? 🍵', ]; @override void initState() { super.initState(); // Muat riwayat dari Firestore saat pertama kali dibuka WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(amorChatProvider.notifier).loadHistory(); }); } @override void dispose() { _chatController.dispose(); _scrollCtrl.dispose(); super.dispose(); } // ── Send ─────────────────────────────────────────────────────────────────── Future _sendMessage() async { final message = _chatController.text.trim(); final notifier = ref.read(amorChatProvider.notifier); final isLoading = ref.read(amorChatProvider).isLoading; if (message.isEmpty || isLoading) return; _chatController.clear(); notifier.addUserMessage(message); notifier.setLoading(true); _scrollToBottom(); try { final reply = await GeminiService().chatWithMenu( message: message, menuContext: _amorMenu, history: notifier.aiHistory, ); await notifier.addAIMessage(reply); } catch (e) { await notifier.addAIMessage('Maaf, terjadi kesalahan. Coba lagi ya! 😅'); } finally { notifier.setLoading(false); _scrollToBottom(); } } void _sendSuggestion(String s) { _chatController.text = s.replaceAll(RegExp(r'\s+\S+$'), '').trim(); _sendMessage(); } void _scrollToBottom() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollCtrl.hasClients) { _scrollCtrl.animateTo( _scrollCtrl.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } // ── Build ────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final chatState = ref.watch(amorChatProvider); final messages = chatState.messages; final isLoading = chatState.isLoading; return Scaffold( backgroundColor: AppTheme.warmWhite, appBar: _buildAppBar(messages.isNotEmpty), body: Column( children: [ Expanded( child: messages.isEmpty && !isLoading ? _buildPlaceholder() : _buildMessageList(messages, isLoading), ), _buildChatInput(isLoading), ], ), ); } // ── AppBar ───────────────────────────────────────────────────────────────── PreferredSizeWidget _buildAppBar(bool hasHistory) { return AppBar( backgroundColor: Colors.white, foregroundColor: AppTheme.primaryBrown, elevation: 0, surfaceTintColor: Colors.white, title: Row( children: [ CircleAvatar( radius: 16, backgroundColor: AppTheme.primaryBrown, child: const Icon(Icons.smart_toy, size: 16, color: Colors.white), ), const SizedBox(width: 10), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Amori', overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: AppTheme.primaryBrown, fontWeight: FontWeight.bold, ), ), Text( 'Asisten Menu Amor Coffee', overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.grey500, fontSize: 11, ), ), ], ), ), ], ), actions: [ IconButton( icon: const Icon(Icons.menu_book_outlined), tooltip: 'Lihat daftar menu', color: AppTheme.primaryBrown, onPressed: _showMenuSheet, ), if (hasHistory) IconButton( icon: const Icon(Icons.delete_outline), tooltip: 'Hapus riwayat chat', color: AppTheme.primaryBrown, onPressed: _confirmClear, ), ], bottom: PreferredSize( preferredSize: const Size.fromHeight(1), child: Divider(height: 1, color: AppTheme.grey200), ), ); } Future _confirmClear() async { final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Hapus Riwayat Chat?'), content: const Text( 'Semua riwayat percakapan dengan Amori akan dihapus permanen.'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Batal')), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('Hapus', style: TextStyle(color: Colors.red))), ], ), ); if (ok == true) { await ref.read(amorChatProvider.notifier).clearHistory(); } } // ── Message List ─────────────────────────────────────────────────────────── Widget _buildMessageList(List messages, bool isLoading) { return ListView.builder( controller: _scrollCtrl, padding: const EdgeInsets.all(AppConstants.defaultPadding), itemCount: messages.length + (isLoading ? 1 : 0), itemBuilder: (context, i) { if (i == messages.length) return _buildTypingIndicator(); return _buildChatMessage(messages[i]); }, ); } // ── Placeholder ──────────────────────────────────────────────────────────── Widget _buildPlaceholder() { return SingleChildScrollView( child: Column( children: [ const SizedBox(height: 40), Icon(Icons.chat_bubble_outline, size: 64, color: AppTheme.grey400), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( 'Halo! Senang sekali bisa membantu Anda mengetahui menu Amor Coffee.', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: AppTheme.grey800, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( 'Ajukan pertanyaan tentang menu, harga, atau minta rekomendasi sesuai selera kamu', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(color: AppTheme.grey500), textAlign: TextAlign.center, ), ), const SizedBox(height: 28), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Wrap( spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: _suggestions.map((s) { return GestureDetector( onTap: () => _sendSuggestion(s), child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 8), decoration: BoxDecoration( color: Colors.white, border: Border.all( color: AppTheme.primaryBrown.withOpacity(0.3)), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Text( s, style: TextStyle( fontSize: 12, fontFamily: 'Poppins', color: AppTheme.primaryBrown, ), ), ), ); }).toList(), ), ), const SizedBox(height: 40), ], ), ); } // ── Chat Bubble ──────────────────────────────────────────────────────────── Widget _buildChatMessage(AmorChatEntry message) { final isUser = message.isUser; return Container( margin: const EdgeInsets.only(bottom: 16), child: Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isUser) ...[ CircleAvatar( radius: 16, backgroundColor: AppTheme.primaryBrown, child: const Icon(Icons.smart_toy, size: 16, color: Colors.white), ), const SizedBox(width: 8), ], Flexible( child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isUser ? AppTheme.primaryBrown : Colors.white, borderRadius: BorderRadius.circular(16).copyWith( bottomLeft: isUser ? const Radius.circular(16) : const Radius.circular(4), bottomRight: isUser ? const Radius.circular(4) : const Radius.circular(16), ), border: isUser ? null : Border.all(color: AppTheme.grey300, width: 1), boxShadow: isUser ? null : [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Text( message.text, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: isUser ? Colors.white : Colors.black87, fontSize: 14, fontWeight: FontWeight.w400, ), ), ), const SizedBox(height: 2), Text( _formatTime(message.time), style: TextStyle( fontSize: 10, color: AppTheme.grey500, fontFamily: 'Poppins', ), ), ], ), ), if (isUser) ...[ const SizedBox(width: 8), CircleAvatar( radius: 16, backgroundColor: AppTheme.grey300, child: const Icon(Icons.person, size: 16, color: Colors.white), ), ], ], ), ).animate().fade().slideY(begin: 0.2, end: 0, curve: Curves.easeOut); } String _formatTime(DateTime t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}'; // ── Typing Indicator ─────────────────────────────────────────────────────── Widget _buildTypingIndicator() { return Container( margin: const EdgeInsets.only(bottom: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( radius: 16, backgroundColor: AppTheme.primaryBrown, child: const Icon(Icons.smart_toy, size: 16, color: Colors.white), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), bottomRight: Radius.circular(16), bottomLeft: Radius.circular(4), ), border: Border.all(color: AppTheme.grey300, width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: List.generate(3, (i) { return Container( margin: const EdgeInsets.symmetric(horizontal: 3), width: 7, height: 7, decoration: BoxDecoration( color: AppTheme.primaryBrown.withOpacity(0.5), shape: BoxShape.circle, ), ) .animate(onPlay: (c) => c.repeat()) .scaleXY( delay: Duration(milliseconds: i * 150), duration: 500.ms, begin: 0.6, end: 1.0) .then() .scaleXY(duration: 500.ms, begin: 1.0, end: 0.6); }), ), ), ], ), ); } // ── Input Bar ───────────────────────────────────────────────────────────── Widget _buildChatInput(bool isLoading) { return Container( color: Colors.white, child: Container( padding: const EdgeInsets.all(AppConstants.defaultPadding), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, -2), ), ], ), child: Row( children: [ Expanded( child: TextField( controller: _chatController, onSubmitted: (_) => _sendMessage(), style: const TextStyle(color: Colors.black, fontSize: 16), maxLines: null, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( hintText: 'Tanya tentang menu Amor Coffee...', hintStyle: TextStyle( color: AppTheme.grey500, fontSize: 14, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, ), filled: true, fillColor: AppTheme.grey50, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), ), ), const SizedBox(width: 8), Container( decoration: const BoxDecoration( color: AppTheme.primaryBrown, shape: BoxShape.circle, ), child: IconButton( onPressed: isLoading ? null : _sendMessage, icon: isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white), ), ) : const Icon(Icons.send, color: Colors.white), ), ), ], ), ), ); } // ── Menu Sheet ───────────────────────────────────────────────────────────── void _showMenuSheet() { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => DraggableScrollableSheet( initialChildSize: 0.85, maxChildSize: 0.95, minChildSize: 0.4, builder: (ctx, ctrl) => Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ Container( margin: const EdgeInsets.only(top: 12, bottom: 8), width: 40, height: 4, decoration: BoxDecoration( color: AppTheme.grey300, borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 8), child: Row( children: [ Icon(Icons.menu_book_outlined, color: AppTheme.primaryBrown), const SizedBox(width: 8), Text( 'Daftar Menu Amor Coffee', style: Theme.of(context) .textTheme .titleMedium ?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryBrown, ), ), ], ), ), Divider(height: 1, color: AppTheme.grey200), Expanded( child: SingleChildScrollView( controller: ctrl, padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _menuCat('☕ Signature', [('Teh Sereh Lemon', 10000), ('StrawCano', 13000), ('Es Kopi Susu Amor', 13000), ('Avocalypse', 15000)]), _menuCat('🫗 Manual Brew', [('V60/Kopi Filter', 15000), ('Kopi Tubruk', 7000)]), _menuCat('☕ Coffee', [('Americano', 10000), ('Aren Latte', 13000), ('Caramel Latte', 14000), ('Hazelnut Latte', 14000), ('Vanilla Latte', 14000), ('Cappucino (Hot)', 14000), ('ButterScotch Latte', 18000)]), _menuCat('🥤 Squash', [('Strawberry', 12000), ('Melon', 12000), ('Grape', 12000), ('Lemon', 13000), ('Lychee', 12000)]), _menuCat('🍵 Tea', [('Original Tea', 7000), ('Strawberry Tea', 10000), ('Lychee Tea', 10000), ('Lemon Tea', 10000), ('Thai Tea', 12000)]), _menuCat('🥛 Milk Based', [('Avocado', 12000), ('Taro', 12000), ('Redvelvet', 12000), ('Pink Lava', 12000), ('Chocolate', 12000), ('Choco Caramel', 13000), ('Choco Hazelnut', 13000), ('Matcha', 12000), ('Matcha Coconut', 14000)]), _menuCat('💧 Others', [('Extra Shot Espresso', 4000), ('Mineral', 5000)]), _menuCat('🍽️ Food', [('Nasilor (Nasi Sosis Telor)', 10000), ('Chicken Katsu Mentai', 12000), ('Chicken Katsu Blackpapper', 12000), ('Chicken Katsu Spicy', 12000), ('Indomie Kuah Amor', 12000), ('Indomie Goreng Amor', 15000)]), _menuCat('🍟 Snack', [('Donat Amor', 10000), ('French Fries', 10000), ('Ropang manis', 10000), ('Ropang Telur', 11000), ('Ropang Beef', 13000), ('Pisang Choco', 12000), ('Tempe Mendoan', 12000), ('Gyoza', 13000), ('Dimsum Ayam', 13000), ('Dimsum Mentai', 15000), ('Mix Sampler', 15000)]), ], ), ), ), ], ), ), ), ); } Widget _menuCat(String title, List<(String, int)> items) { String fmt(int p) => p.toString().replaceAllMapped( RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (m) => '${m[1]}.'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 14), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppTheme.primaryBrown.withOpacity(0.08), borderRadius: BorderRadius.circular(8), ), child: Text( title, style: Theme.of(context).textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryBrown, ), ), ), const SizedBox(height: 6), ...items.map((item) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text(item.$1, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( color: const Color(0xFF2D1B0A))), ), Text( 'Rp ${fmt(item.$2)}', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith( fontWeight: FontWeight.w600, color: AppTheme.primaryBrown, ), ), ], ), )), Divider(height: 1, color: AppTheme.grey200), ], ); } }