import 'package:flutter/material.dart'; import 'package:tablet_app/Models/AssistantResponse.dart'; import 'package:tablet_app/Models/tabletContext.dart'; import 'package:tablet_app/Services/assistantService.dart'; import 'package:tablet_app/constants.dart'; class AssistantChatView extends StatefulWidget { final TabletAppContext tabletAppContext; final String? configurationId; final void Function(String sectionId, String sectionTitle)? onNavigateToSection; const AssistantChatView({ Key? key, required this.tabletAppContext, this.configurationId, this.onNavigateToSection, }) : super(key: key); @override State createState() => _AssistantChatViewState(); } class _AssistantChatViewState extends State { late AssistantService _assistantService; final TextEditingController _controller = TextEditingController(); final ScrollController _scrollController = ScrollController(); final List _bubbles = []; bool _isLoading = false; @override void initState() { super.initState(); _assistantService = AssistantService(tabletAppContext: widget.tabletAppContext); } Future _send() async { final text = _controller.text.trim(); if (text.isEmpty || _isLoading) return; _controller.clear(); setState(() { _bubbles.add(_ChatBubble(text: text, isUser: true)); _isLoading = true; }); _scrollToBottom(); try { final response = await _assistantService.chat( message: text, configurationId: widget.configurationId, ); setState(() { _bubbles.add(_AssistantMessage( response: response, onNavigate: widget.onNavigateToSection, )); }); } catch (_) { setState(() { _bubbles.add(_ChatBubble(text: "Une erreur est survenue, réessayez.", isUser: false)); }); } finally { setState(() => _isLoading = false); _scrollToBottom(); } } void _scrollToBottom() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } @override Widget build(BuildContext context) { return Container( width: 480, height: double.infinity, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.horizontal(left: Radius.circular(20)), boxShadow: [ BoxShadow(color: Colors.black26, blurRadius: 12, offset: Offset(-4, 0)), ], ), child: Column( children: [ // Header Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), decoration: BoxDecoration( color: kMainGrey, borderRadius: const BorderRadius.only(topLeft: Radius.circular(20)), ), child: Row( children: [ const Icon(Icons.chat_bubble_outline, color: Colors.white, size: 22), const SizedBox(width: 10), const Expanded( child: Text( "Assistant", style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600), ), ), IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), ], ), ), // Messages Expanded( child: _bubbles.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(32), child: Text( "Bonjour ! Posez-moi vos questions sur cette visite.", textAlign: TextAlign.center, style: TextStyle(color: Colors.grey[500], fontSize: 18), ), ), ) : ListView.builder( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), itemCount: _bubbles.length, itemBuilder: (_, i) => _bubbles[i], ), ), // Loading if (_isLoading) Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Row( children: [ SizedBox( width: 22, height: 22, child: CircularProgressIndicator(strokeWidth: 2, color: kMainGrey), ), const SizedBox(width: 10), Text("...", style: TextStyle(color: Colors.grey[400], fontSize: 16)), ], ), ), // Input Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Expanded( child: TextField( controller: _controller, textCapitalization: TextCapitalization.sentences, style: const TextStyle(fontSize: 16), decoration: InputDecoration( hintText: "Votre question...", filled: true, fillColor: Colors.grey[100], border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), ), onSubmitted: (_) => _send(), ), ), const SizedBox(width: 10), CircleAvatar( radius: 26, backgroundColor: kMainGrey, child: IconButton( icon: const Icon(Icons.send, color: Colors.white, size: 20), onPressed: _send, ), ), ], ), ), ], ), ); } } class _ChatBubble extends StatelessWidget { final String text; final bool isUser; const _ChatBubble({required this.text, required this.isUser}); @override Widget build(BuildContext context) { return Align( alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.symmetric(vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.65), decoration: BoxDecoration( color: isUser ? kMainGrey : Colors.grey[100], borderRadius: BorderRadius.only( topLeft: const Radius.circular(18), topRight: const Radius.circular(18), bottomLeft: isUser ? const Radius.circular(18) : const Radius.circular(4), bottomRight: isUser ? const Radius.circular(4) : const Radius.circular(18), ), ), child: Text( text, style: TextStyle( color: isUser ? Colors.white : kSecondGrey, fontSize: 15, ), ), ), ); } } class _AssistantMessage extends StatelessWidget { final AssistantResponse response; final void Function(String sectionId, String sectionTitle)? onNavigate; const _AssistantMessage({required this.response, this.onNavigate}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Text bubble _ChatBubble(text: response.reply, isUser: false), // Cards if (response.cards != null && response.cards!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 6, left: 4, right: 32), child: Column( children: response.cards! .map((card) => _AiCardWidget(card: card)) .toList(), ), ), // Navigation button if (response.navigation != null && onNavigate != null) Padding( padding: const EdgeInsets.only(top: 8, left: 4), child: ElevatedButton.icon( onPressed: () => onNavigate!( response.navigation!.sectionId, response.navigation!.sectionTitle, ), icon: const Icon(Icons.arrow_forward, size: 18), label: Text(response.navigation!.sectionTitle), style: ElevatedButton.styleFrom( backgroundColor: kMainGrey, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), ), ), ), ], ), ); } } class _AiCardWidget extends StatelessWidget { final AiCard card; const _AiCardWidget({required this.card}); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[200]!), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2)), ], ), child: Row( children: [ if (card.icon != null) ...[ Text(card.icon!, style: const TextStyle(fontSize: 20)), const SizedBox(width: 10), ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( card.title, style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: kSecondGrey), ), if (card.subtitle.isNotEmpty) Text( card.subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[500]), ), ], ), ), ], ), ); } }