import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/exceptions.dart'; import '../../data/models/message.dart'; import '../../providers/briefing_provider.dart'; import '../../widgets/briefing_digest_card.dart'; import '../../widgets/chat_message_bubble.dart'; import 'briefing_history_screen.dart'; class BriefingScreen extends ConsumerStatefulWidget { const BriefingScreen({super.key}); @override ConsumerState createState() => _BriefingScreenState(); } class _BriefingScreenState extends ConsumerState { final _controller = TextEditingController(); final _scrollController = ScrollController(); bool _refreshing = false; @override void dispose() { _controller.dispose(); _scrollController.dispose(); super.dispose(); } void _scrollToBottom() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 200), curve: Curves.easeOut, ); } }); } Future _sendReply() async { final text = _controller.text.trim(); if (text.isEmpty) return; _controller.clear(); try { await ref.read(briefingProvider.notifier).sendReply(text); } on AppException catch (e) { if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(e.message))); } } catch (_) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to send reply.')), ); } } } Future _refresh() async { setState(() => _refreshing = true); try { await ref.read(briefingProvider.notifier).refresh('compilation'); } catch (_) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Could not generate briefing.')), ); } } finally { if (mounted) setState(() => _refreshing = false); } } @override Widget build(BuildContext context) { final briefingAsync = ref.watch(briefingProvider); final isStreaming = ref.watch(isBriefingStreamingProvider); final scheme = Theme.of(context).colorScheme; // Scroll to bottom when messages change ref.listen(briefingProvider, (_, _) => _scrollToBottom()); return Scaffold( appBar: AppBar( title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Briefing', style: Theme.of(context).textTheme.titleLarge), Text( _todayLabel(), style: Theme.of(context).textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ], ), actions: [ if (_refreshing) const Padding( padding: EdgeInsets.symmetric(horizontal: 12), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ) else IconButton( icon: const Icon(Icons.refresh_outlined), tooltip: 'Generate briefing', onPressed: _refresh, ), PopupMenuButton( onSelected: (value) { if (value == 'history') { Navigator.of(context).push(MaterialPageRoute( builder: (_) => const BriefingHistoryScreen(), )); } }, itemBuilder: (_) => const [ PopupMenuItem( value: 'history', child: Text('View past briefings'), ), ], ), ], ), body: briefingAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, _) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text("Could not load today's briefing."), const SizedBox(height: 12), FilledButton.tonal( onPressed: () => ref.invalidate(briefingProvider), child: const Text('Retry'), ), ], ), ), data: (conv) { // First assistant message for the digest card (null if none yet) final Message? firstAssistant = conv.messages .where((m) => m.role == MessageRole.assistant) .toList() .firstOrNull; return Column( children: [ // Digest card header BriefingDigestCard( message: firstAssistant, onGenerateNow: _refresh, ), // Divider + label if (conv.messages.isNotEmpty) ...[ const SizedBox(height: 4), Row(children: [ const Expanded(child: Divider()), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Text( 'Conversation', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ), const Expanded(child: Divider()), ]), ], // Message list Expanded( child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 8), itemCount: conv.messages.length, itemBuilder: (_, i) => ChatMessageBubble(message: conv.messages[i]), ), ), // Progress bar while streaming if (isStreaming) LinearProgressIndicator( minHeight: 2, color: scheme.primary, ), // Reply bar const Divider(height: 1), SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), child: Row( children: [ Expanded( child: TextField( controller: _controller, decoration: const InputDecoration( hintText: 'Reply to your briefing…', border: OutlineInputBorder(), isDense: true, contentPadding: EdgeInsets.symmetric( horizontal: 12, vertical: 10), ), minLines: 1, maxLines: 4, textInputAction: TextInputAction.newline, enabled: !isStreaming, ), ), const SizedBox(width: 8), _GradientSendButton( onPressed: isStreaming ? null : _sendReply, isStreaming: isStreaming, ), ], ), ), ), ], ); }, ), ); } String _todayLabel() { final now = DateTime.now(); const days = [ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ]; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; return '${days[now.weekday - 1]}, ${months[now.month - 1]} ${now.day}'; } } class _GradientSendButton extends StatelessWidget { final VoidCallback? onPressed; final bool isStreaming; const _GradientSendButton({ required this.onPressed, required this.isStreaming, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final disabled = onPressed == null; return DecoratedBox( decoration: BoxDecoration( gradient: disabled ? null : const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF6366F1), Color(0xFF4F46E5)], ), color: disabled ? scheme.onSurface.withValues(alpha: 0.12) : null, borderRadius: BorderRadius.circular(10), ), child: IconButton( icon: isStreaming ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Icon( Icons.send, color: disabled ? scheme.onSurface.withValues(alpha: 0.38) : Colors.white, ), onPressed: onPressed, ), ); } }