import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../providers/auth_provider.dart'; import '../providers/chat_provider.dart'; import '../models/message.dart'; class ChatConversationScreen extends StatefulWidget { final String chatId; const ChatConversationScreen({ super.key, required this.chatId, }); @override State createState() => _ChatConversationScreenState(); } class _ChatConversationScreenState extends State { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); _loadChat(); } @override void dispose() { _messageController.dispose(); _scrollController.dispose(); super.dispose(); } Future _loadChat() async { final authProvider = Provider.of(context, listen: false); final chatProvider = Provider.of(context, listen: false); final accessToken = await authProvider.getValidAccessToken(); if (accessToken == null) { await authProvider.logout(); return; } await chatProvider.fetchChat( accessToken: accessToken, chatId: widget.chatId, ); // Scroll to bottom after loading WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); } }); } Future _sendMessage() async { final content = _messageController.text.trim(); if (content.isEmpty) return; final authProvider = Provider.of(context, listen: false); final chatProvider = Provider.of(context, listen: false); final accessToken = await authProvider.getValidAccessToken(); if (accessToken == null) { await authProvider.logout(); return; } // Clear input field immediately _messageController.clear(); await chatProvider.sendMessage( accessToken: accessToken, chatId: widget.chatId, content: content, ); // Scroll to bottom after sending WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } Future _editTitle() async { final chatProvider = Provider.of(context, listen: false); final currentTitle = chatProvider.currentChat?.title ?? ''; final newTitle = await showDialog( context: context, builder: (context) { final controller = TextEditingController(text: currentTitle); return AlertDialog( title: const Text('Edit Title'), content: TextField( controller: controller, decoration: const InputDecoration( labelText: 'Chat Title', border: OutlineInputBorder(), ), autofocus: true, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(context, controller.text.trim()), child: const Text('Save'), ), ], ); }, ); if (newTitle != null && newTitle.isNotEmpty && newTitle != currentTitle && mounted) { final authProvider = Provider.of(context, listen: false); final accessToken = await authProvider.getValidAccessToken(); if (accessToken != null) { await chatProvider.updateChatTitle( accessToken: accessToken, chatId: widget.chatId, title: newTitle, ); } } } String _formatTime(DateTime dateTime) { final hour = dateTime.hour.toString().padLeft(2, '0'); final minute = dateTime.minute.toString().padLeft(2, '0'); return '$hour:$minute'; } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( title: Consumer( builder: (context, chatProvider, _) { return GestureDetector( onTap: _editTitle, child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( chatProvider.currentChat?.title ?? 'Chat', overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 4), const Icon(Icons.edit, size: 18), ], ), ); }, ), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _loadChat, tooltip: 'Refresh', ), ], ), body: Consumer( builder: (context, chatProvider, _) { if (chatProvider.isLoading && chatProvider.currentChat == null) { return const Center( child: CircularProgressIndicator(), ); } if (chatProvider.errorMessage != null && chatProvider.currentChat == null) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 64, color: colorScheme.error, ), const SizedBox(height: 16), Text( 'Failed to load chat', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( chatProvider.errorMessage!, style: TextStyle(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: _loadChat, icon: const Icon(Icons.refresh), label: const Text('Try Again'), ), ], ), ), ); } final messages = chatProvider.currentChat?.messages ?? []; return Column( children: [ // Messages list Expanded( child: messages.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.chat_bubble_outline, size: 64, color: colorScheme.onSurfaceVariant, ), const SizedBox(height: 16), Text( 'Start a conversation', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'Send a message to begin chatting with the AI assistant.', style: TextStyle(color: colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], ), ) : ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(16), itemCount: messages.length, itemBuilder: (context, index) { final message = messages[index]; return _MessageBubble( message: message, formatTime: _formatTime, ); }, ), ), // Loading indicator when sending if (chatProvider.isSendingMessage) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 12), Text( 'AI is thinking...', style: TextStyle( color: colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, ), ), ], ), ), // Message input Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surface, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, -2), ), ], ), child: Row( children: [ Expanded( child: TextField( controller: _messageController, decoration: InputDecoration( hintText: 'Type a message...', border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), maxLines: null, textCapitalization: TextCapitalization.sentences, onSubmitted: (_) => _sendMessage(), ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.send), onPressed: chatProvider.isSendingMessage ? null : _sendMessage, color: colorScheme.primary, iconSize: 28, ), ], ), ), ], ); }, ), ); } } class _MessageBubble extends StatelessWidget { final Message message; final String Function(DateTime) formatTime; const _MessageBubble({ required this.message, required this.formatTime, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final isUser = message.isUser; return Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isUser) CircleAvatar( radius: 16, backgroundColor: colorScheme.primaryContainer, child: Icon( Icons.smart_toy, size: 18, color: colorScheme.onPrimaryContainer, ), ), const SizedBox(width: 8), Flexible( child: Column( crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: isUser ? colorScheme.primary : colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( message.content, style: TextStyle( color: isUser ? colorScheme.onPrimary : colorScheme.onSurfaceVariant, ), ), if (message.toolCalls != null && message.toolCalls!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 8), child: Wrap( spacing: 4, runSpacing: 4, children: message.toolCalls!.map((toolCall) { return Chip( label: Text( toolCall.functionName, style: const TextStyle(fontSize: 11), ), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, ); }).toList(), ), ), ], ), ), const SizedBox(height: 4), Text( formatTime(message.createdAt), style: TextStyle( fontSize: 11, color: colorScheme.onSurfaceVariant, ), ), ], ), ), const SizedBox(width: 8), if (isUser) CircleAvatar( radius: 16, backgroundColor: colorScheme.primary, child: Icon( Icons.person, size: 18, color: colorScheme.onPrimary, ), ), ], ), ); } }