// ignore_for_file: library_private_types_in_public_api, deprecated_member_use, depend_on_referenced_packages import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/main.dart'; import 'package:didvan/models/ai/ai_chat_args.dart'; import 'package:didvan/models/ai/chats_model.dart'; import 'package:didvan/models/ai/messages_model.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/alert_data.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/utils/date_time.dart'; import 'package:didvan/views/ai/ai_chat_state.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/marquee_text.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; import 'package:path/path.dart' as p; class AiChatPage extends StatefulWidget { final AiChatArgs args; const AiChatPage({Key? key, required this.args}) : super(key: key); @override _AiChatPageState createState() => _AiChatPageState(); } class _AiChatPageState extends State { TextEditingController message = TextEditingController(); @override void initState() { final state = context.read(); state.chatId = widget.args.chatId; if (state.chatId != null) { state.getAllMessages(state.chatId!); } super.initState(); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { context.read().getChats(); return true; }, child: Scaffold( appBar: AppBar( shadowColor: Theme.of(context).colorScheme.border, title: Row( children: [ Padding( padding: const EdgeInsets.all(12.0), child: ClipOval( child: CachedNetworkImage( width: 32, height: 32, imageUrl: widget.args.bot.image.toString(), ), ), ), Text('چت با ${widget.args.bot.name}'), ], ), automaticallyImplyLeading: false, actions: [ DidvanIconButton( icon: DidvanIcons.angle_left_regular, onPressed: () { context.read().getChats(); navigatorKey.currentState!.pop(); }, ) ], ), body: Consumer( builder: (BuildContext context, AiChatState state, Widget? child) => state.loading ? Center( child: Image.asset( Assets.loadingAnimation, width: 60, height: 60, ), ) : state.messages.isEmpty ? Center( child: EmptyState( asset: Assets.emptyChat, title: 'اولین پیام را بنویسید...', ), ) : SingleChildScrollView( reverse: true, controller: state.scrollController, child: ListView.builder( itemCount: state.messages.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(bottom: 90), itemBuilder: (context, mIndex) { final prompts = state.messages[mIndex].prompts; final time = state.messages[mIndex].dateTime; return Column( children: [ timeLabel(context, time), ListView.builder( itemCount: prompts.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final message = prompts[index]; return messageBubble(message, context, state, index, mIndex); }, ), ], ); }), ), ), bottomSheet: Consumer( builder: (BuildContext context, AiChatState state, Widget? child) { return Container( width: MediaQuery.sizeOf(context).width, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), decoration: BoxDecoration( border: Border( top: BorderSide( color: Theme.of(context).colorScheme.cardBorder, ), ), color: Theme.of(context).colorScheme.surface, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (widget.args.bot.attachment! == 2) fileContainer(context), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox( width: 12, ), SizedBox( width: 46, height: 46, child: Center( child: state.onResponsing ? Center( child: SpinKitThreeBounce( size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ) : DidvanIconButton( icon: DidvanIcons.send_solid, size: 32, color: Theme.of(context) .colorScheme .focusedBorder, onPressed: () async { if (state.file == null && message.text.isEmpty) { return; } if (state.messages.isNotEmpty && DateTime.parse( state.messages.last.dateTime) .toPersianDateStr() .contains(DateTime.parse( DateTime.now() .subtract( const Duration( minutes: 210)) .toIso8601String()) .toPersianDateStr())) { state.messages.last.prompts.add(Prompts( text: message.text, file: p.basename(state.file!.path), fileName: p.basename(state.file!.path), finished: true, role: 'user', createdAt: DateTime.now() .subtract( const Duration(minutes: 210)) .toIso8601String(), )); } else { state.messages.add(MessageModel( dateTime: DateTime.now() .subtract( const Duration(minutes: 210)) .toIso8601String(), prompts: [ Prompts( text: message.text, finished: true, file: p.basename(state.file!.path), fileName: p.basename(state.file!.path), role: 'user', createdAt: DateTime.now() .subtract(const Duration( minutes: 210)) .toIso8601String(), ) ])); } await state.postMessage(widget.args.bot); message.clear(); state.file = null; }, ), ), ), const SizedBox( width: 12, ), Expanded( child: Form( child: widget.args.bot.attachment! != 1 ? TextFormField( textInputAction: TextInputAction.newline, style: Theme.of(context).textTheme.bodyMedium, maxLines: 6, minLines: 1, // keyboardType: TextInputType.text, controller: message, enabled: !state.onResponsing, decoration: InputDecoration( border: InputBorder.none, hintText: 'بنویسید...', hintStyle: Theme.of(context) .textTheme .bodySmall! .copyWith( color: Theme.of(context) .colorScheme .disabledText), ), onChanged: (value) {}, ) : fileContainer(context), ), ), const SizedBox( width: 12, ), if (widget.args.bot.attachment! != 0) SizedBox( width: 46, height: 46, child: Center( child: PopupMenuButton( onSelected: (value) async { switch (value) { case 'Pdf': FilePickerResult? result = await MediaService.pickPdfFile(); if (result != null) { final File file = File(result.files.single.path!); state.file = file; // Do something with the selected PDF file } // else { //// User cancelled the file selection // } break; case 'Image': final pickedFile = await MediaService.pickImage( source: ImageSource.gallery); File? file; if (pickedFile != null && !kIsWeb) { file = await ImageCropper().cropImage( sourcePath: pickedFile.path, androidUiSettings: const AndroidUiSettings( toolbarTitle: 'برش تصویر'), iosUiSettings: const IOSUiSettings( title: 'برش تصویر', doneButtonTitle: 'تایید', cancelButtonTitle: 'بازگشت', ), compressQuality: 30, ); if (file == null) return; } if (pickedFile == null) return; state.file = kIsWeb ? File(pickedFile.path) : file; break; default: } state.update(); }, itemBuilder: (BuildContext context) => [ popUpBtns(value: 'Pdf'), popUpBtns(value: 'Image'), ], offset: const Offset(0, -140), position: PopupMenuPosition.over, child: Icon( Icons.attach_file_rounded, color: Theme.of(context).colorScheme.focusedBorder, ), ), ), ), ], ), ], )); }), ), ); } AnimatedVisibility fileContainer(BuildContext context) { final state = context.read(); String basename = ''; if (state.file != null) { basename = p.basename(state.file!.path); } return AnimatedVisibility( isVisible: state.file != null, duration: DesignConfig.lowAnimationDuration, child: Container( decoration: BoxDecoration( borderRadius: DesignConfig.mediumBorderRadius, color: Theme.of(context).colorScheme.border, ), padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), margin: widget.args.bot.attachment! != 1 ? const EdgeInsets.symmetric(horizontal: 12) : EdgeInsets.zero, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Icon(Icons.file_copy), const SizedBox( width: 12, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 160, height: 24, child: MarqueeText( text: basename, style: const TextStyle(fontSize: 14), stop: const Duration(seconds: 3), ), ), if (state.file != null) FutureBuilder( future: state.file!.length(), builder: (context, snapshot) { if (!snapshot.hasData) { return const SizedBox(); } return DidvanText( 'File Size ${(snapshot.data! / 1000).round()} KB', fontSize: 12, ); }) ], ) ], ), InkWell( onTap: () { state.file = null; state.update(); }, child: const Icon(DidvanIcons.close_circle_solid)) ], ), ), ); } PopupMenuItem popUpBtns({required final String value}) { return PopupMenuItem( value: value, height: 46, child: Row( children: [ const Icon(Icons.picture_as_pdf_rounded), const SizedBox( width: 12, ), DidvanText( value, ), ], ), ); } Center timeLabel(BuildContext context, String time) { return Center( child: Container( margin: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.splash, borderRadius: DesignConfig.lowBorderRadius, ), child: DidvanText( DateTime.parse(time).toPersianDateStr(), style: Theme.of(context).textTheme.labelSmall, color: DesignConfig.isDark ? Theme.of(context).colorScheme.white : Theme.of(context).colorScheme.black, ), ), ); } Padding messageBubble(Prompts message, BuildContext context, AiChatState state, int index, int mIndex) { MarkdownStyleSheet defaultMarkdownStyleSheet = MarkdownStyleSheet( code: TextStyle( backgroundColor: Theme.of(context).colorScheme.black, color: Theme.of(context).colorScheme.white, ), codeblockPadding: const EdgeInsets.all(8), codeblockDecoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: Theme.of(context).colorScheme.black)); return Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: message.role.toString().contains('user') ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ Row( mainAxisAlignment: message.role.toString().contains('user') ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( borderRadius: DesignConfig.mediumBorderRadius.copyWith( bottomLeft: !message.role.toString().contains('user') ? Radius.zero : null, bottomRight: message.role.toString().contains('user') ? Radius.zero : null, ), color: (message.role.toString().contains('user') ? Theme.of(context).colorScheme.surface : Theme.of(context).colorScheme.focused) .withOpacity(0.9), border: Border.all( color: Theme.of(context).colorScheme.border, width: 0.5, ), ), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.sizeOf(context).width / 1.5), child: message.finished != null && !message.finished! ? StreamBuilder( stream: state.messageOnstream, builder: (context, snapshot) { if (!snapshot.hasData) { return const SizedBox(); } return Markdown( data: "${snapshot.data}...", selectable: false, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), styleSheet: defaultMarkdownStyleSheet); }, ) : Column( children: [ if (message.role.toString().contains('user') && message.file != null) Container( decoration: BoxDecoration( borderRadius: DesignConfig.mediumBorderRadius, color: Theme.of(context).colorScheme.border, ), padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), margin: widget.args.bot.attachment! != 1 ? const EdgeInsets.symmetric(horizontal: 12) : EdgeInsets.zero, child: Row( children: [ const Icon(Icons.file_copy), const SizedBox( width: 12, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 200, child: DidvanText( (message.fileName.toString())), ), if (state.file != null) FutureBuilder( future: state.file!.length(), builder: (context, snapshot) { if (!snapshot.hasData) { return const SizedBox(); } return DidvanText( 'File Size ${(snapshot.data! / 1000).round()} KB', fontSize: 12, ); }) ], ) ], ), ), if (message.text != null) Markdown( data: message.text.toString(), selectable: true, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), styleSheet: defaultMarkdownStyleSheet, ), if (!message.role.toString().contains('user')) Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { await Clipboard.setData(ClipboardData( text: state.messages[mIndex] .prompts[index].text .toString())); ActionSheetUtils.showAlert(AlertData( message: "متن با موفقیت کپی شد", aLertType: ALertType.success)); }, child: Icon( DidvanIcons.copy_regular, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ) ], ) ], ), ), ) ], ), const SizedBox(height: 4), Row( mainAxisSize: MainAxisSize.min, children: [ DidvanText( DateTimeUtils.timeWithAmPm(message.createdAt.toString()), // DateTimeUtils.timeWithAmPm(message.createdAt), style: Theme.of(context).textTheme.labelSmall, color: Theme.of(context).colorScheme.caption, ), ], ), ], ), ); } }