// ignore_for_file: library_private_types_in_public_api, deprecated_member_use, depend_on_referenced_packages 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/files_model.dart'; import 'package:didvan/models/ai/messages_model.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; import 'package:didvan/models/view/alert_data.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.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/ai/widgets/ai_message_bar.dart'; import 'package:didvan/views/ai/widgets/audio_wave.dart'; import 'package:didvan/views/widgets/didvan/button.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/skeleton_image.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:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; 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 { FocusNode focusNode = FocusNode(); @override void initState() { final state = context.read(); if (widget.args.chat != null) { state.chatId = widget.args.chat!.id!; state.chat = widget.args.chat; } // JsInteropService().showAlert(); WidgetsBinding.instance.addPostFrameCallback((_) async { if (state.chatId != null) { state.getAllMessages(state.chatId!).then((value) => Future.delayed( const Duration( milliseconds: 100, ), () => focusNode.requestFocus(), )); } else { Future.delayed( const Duration( milliseconds: 100, ), () => focusNode.requestFocus(), ); } if (widget.args.prompts != null) { state.messages.add(MessageModel( dateTime: DateTime.now() .subtract(const Duration(minutes: 210)) .toIso8601String(), prompts: [widget.args.prompts!])); state.message.clear(); state.update(); await state.postMessage(widget.args.bot); } }); super.initState(); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { if (context.read().refresh) { context.read().getChats(); context.read().refresh = false; } return true; }, child: Consumer( builder: (context, state, child) => Scaffold( appBar: AppBar( shadowColor: Theme.of(context).colorScheme.border, title: Text(widget.args.bot.name.toString()), leading: Row( children: [ DidvanIconButton( icon: DidvanIcons.angle_right_solid, onPressed: () { Navigator.of(context).pop(); if (context.read().refresh) { context.read().getChats(); context.read().refresh = false; } }, ), ], ), actions: [ if (state.chatId != null) Padding( padding: const EdgeInsets.only(left: 8.0), child: InkWell( onTap: () { final TextEditingController placeholder = TextEditingController( text: state.chat?.placeholder); ActionSheetUtils(context).openDialog( data: ActionSheetData( hasConfirmButtonClose: false, hasConfirmButton: false, hasDismissButton: false, content: ValueListenableBuilder( valueListenable: state.changingPlaceHolder, builder: (context, value, child) => Container( constraints: BoxConstraints( maxHeight: MediaQuery.sizeOf(context) .height / 3), child: Column( children: [ Expanded( child: SingleChildScrollView( child: Column( children: [ Stack( children: [ Row( mainAxisAlignment: MainAxisAlignment .center, children: [ DidvanText( 'شخصی‌سازی دستورات', style: Theme.of( context) .textTheme .titleMedium, ), ], ), Positioned( right: 0, top: 0, bottom: 0, child: Center( child: InkWell( onTap: () { ActionSheetUtils( context) .pop(); }, child: const Icon( DidvanIcons .close_solid, size: 24, ), ), )), ], ), const SizedBox( height: 12, ), const DidvanText( 'دوست دارید هوشان چه چیزهایی را درباره شما بداند تا بتواند پاسخ‌های بهتری ارائه دهد؟ '), const SizedBox( height: 12, ), value ? Center( child: Image.asset( Assets .loadingAnimation, width: 60, height: 60, ), ) : TextField( controller: placeholder, style: (Theme.of( context) .textTheme .bodyMedium)! .copyWith( fontFamily: DesignConfig .fontFamily .padRight( 3)), minLines: 5, maxLines: 5, keyboardType: TextInputType .multiline, decoration: InputDecoration( filled: true, fillColor: Theme.of( context) .colorScheme .secondCTA, contentPadding: const EdgeInsets .fromLTRB( 10, 18, 10, 0), border: const OutlineInputBorder( borderRadius: DesignConfig .lowBorderRadius), errorStyle: const TextStyle( height: 0.01), ), ), ], ), ), ), const SizedBox( height: 12, ), Row( children: [ Expanded( child: DidvanButton( onPressed: () { Navigator.of(context).pop(); }, title: 'بازگشت', style: ButtonStyleMode.secondary, ), ), const SizedBox(width: 20), Expanded( child: DidvanButton( style: ButtonStyleMode.primary, onPressed: () async { final state = context .read(); await state .changePlaceHolder( placeholder.text); Future.delayed( Duration.zero, () => ActionSheetUtils( context) .pop(), ); }, title: 'تایید', ), ), ], ), ], ), ), ))); }, child: const Icon(Icons.shopping_bag_outlined)), ) ], centerTitle: true, automaticallyImplyLeading: false, ), body: state.loading ? Center( child: Image.asset( Assets.loadingAnimation, width: 60, height: 60, ), ) : state.messages.isEmpty ? Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox( height: 12, ), Center( child: Icon( DidvanIcons.ai_solid, size: MediaQuery.sizeOf(context).width / 5, ), ), const DidvanText('هوشان'), const SizedBox( height: 24, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ DidvanText(widget.args.bot.name.toString()), const SizedBox( width: 12, ), ClipOval( child: CachedNetworkImage( width: 46, height: 46, imageUrl: widget.args.bot.image.toString(), ), ), ], ), const SizedBox( height: 24, ), const Padding( padding: EdgeInsets.symmetric(horizontal: 20.0), child: Text( "به هوشان؛ هوش مصنوعی دیدوان خوش آمدید. \nبرای شروع گفتگو پیام مورد نظر خود را در کادر زیر بنویسید.", textAlign: TextAlign.center, ), ) ], ) : SingleChildScrollView( reverse: true, controller: state.scrollController, child: ListView.builder( itemCount: state.messages.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: EdgeInsets.only( bottom: state.file != null && !(state.file!.isRecorded) ? 180 : 100), 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: Column( mainAxisSize: MainAxisSize.min, children: [ // Platform.isIOS // ? AiMessageBarIOS( // bot: widget.args.bot, // ) // : AiMessageBar( bot: widget.args.bot, attch: widget.args.attach, ), ], )), ), ); } 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, ), ), ); } Widget messageBubble(Prompts message, BuildContext context, AiChatState state, int index, int mIndex) { FilesModel? file = message.fileLocal ?? (message.file == null ? null : FilesModel(message.file.toString(), duration: message.duration != null ? Duration(seconds: message.duration!) : null)); MarkdownStyleSheet defaultMarkdownStyleSheet = MarkdownStyleSheet( pPadding: const EdgeInsets.all(0.8), h1Padding: const EdgeInsets.all(0.8), h2Padding: const EdgeInsets.all(0.8), h3Padding: const EdgeInsets.all(0.8), h4Padding: const EdgeInsets.all(0.8), h5Padding: const EdgeInsets.all(0.8), h6Padding: const EdgeInsets.all(0.8), tablePadding: const EdgeInsets.all(0.8), blockquotePadding: const EdgeInsets.all(0.8), listBulletPadding: const EdgeInsets.all(0.8), tableCellsPadding: const EdgeInsets.all(0.8), codeblockPadding: const EdgeInsets.all(8), code: TextStyle( backgroundColor: Theme.of(context).colorScheme.black, color: Theme.of(context).colorScheme.white, ), 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( constraints: BoxConstraints( maxWidth: MediaQuery.sizeOf(context).width / 1.3), 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.error != null && message.error! ? Theme.of(context).colorScheme.error.withOpacity(0.4) : (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( child: message.finished != null && !message.finished! ? Column( children: [ ValueListenableBuilder>( valueListenable: state.messageOnstream, builder: (context, value, child) { return StreamBuilder( stream: value, builder: (context, snapshot) { if (!snapshot.hasData) { return const SizedBox(); } return Markdown( data: "${snapshot.data}...", selectable: false, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), styleSheet: defaultMarkdownStyleSheet); }, ); }), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: SpinKitThreeBounce( color: Theme.of(context).colorScheme.primary, size: 18, ), ) ], ) : Column( children: [ if (file != null) (file.isAudio() // && (!kIsWeb && !Platform.isIOS) ) ? Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), child: AudioWave( file: file.path, totalDuration: file.duration, ), ) : file.isImage() ? Padding( padding: const EdgeInsets.all(8.0), child: messageImage(file), ) : Padding( padding: const EdgeInsets.all( 8.0, ), child: messageFile( context, message, state), ), if (message.text != null && message.text!.isNotEmpty && ((message.audio == null || (message.audio != null && !message.audio!)))) Markdown( data: message.text.toString(), selectable: true, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), styleSheet: defaultMarkdownStyleSheet, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (message.role.toString().contains('user')) PopupMenuButton( offset: const Offset(0, 46), onSelected: (value) async { navigatorKey.currentState!.pushNamed( Routes.aiChat, arguments: AiChatArgs( bot: value, prompts: message)); }, itemBuilder: (BuildContext context) { final historyAiChatState = context .read(); return [ ...List.generate( historyAiChatState.bots.length, (index) => PopupMenuItem( value: historyAiChatState .bots[index], height: 72, child: Container( constraints: const BoxConstraints( maxWidth: 200), child: Row( children: [ ClipOval( child: CachedNetworkImage( imageUrl: historyAiChatState .bots[index] .image .toString(), width: 42, height: 42, ), ), const SizedBox(width: 12), Expanded( child: Directionality( textDirection: TextDirection.ltr, child: DidvanText( historyAiChatState .bots[index] .name .toString(), maxLines: 1, overflow: TextOverflow .ellipsis, ), ), ), ], ), ), ), ) ]; }, child: Container( alignment: Alignment.center, margin: const EdgeInsets.all(8), padding: const EdgeInsets.symmetric( horizontal: 8), constraints: const BoxConstraints( maxWidth: 100), decoration: BoxDecoration( borderRadius: DesignConfig.lowBorderRadius, border: Border.all( color: Theme.of(context) .colorScheme .title)), child: Row( children: [ Expanded( child: Directionality( textDirection: TextDirection.ltr, child: DidvanText( '${widget.args.bot.name}', maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), const Icon( DidvanIcons.angle_down_light), ], ), )), if (message.role .toString() .contains('user') && index == state.messages[mIndex].prompts .length - 2 && (widget.args.bot.editable != null && widget.args.bot.editable!)) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { state.isEdite = true; state.message.text = message.text.toString(); state.update(); }, child: Icon( Icons.edit_outlined, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), if (message.file != null) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { final url = '${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}'; final download = kIsWeb ? MediaService .downloadFileFromWeb(url) : await MediaService.downloadFile( url); AlertData alertData = AlertData( message: 'دانلود موفقیت آمیز بود', aLertType: ALertType.success); if (download == null) { alertData = AlertData( message: 'دانلود موفقیت آمیز نبود', aLertType: ALertType.error); } Future.delayed( Duration.zero, () => ActionSheetUtils(context) .showAlert(alertData), ); }, child: Icon( DidvanIcons.download_solid, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), if (message.error != null && message.error!) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { state.messages.last.prompts .remove(message); state.messages.last.prompts.add( message.copyWith(error: false)); state.file = file; state.update(); await state .postMessage(widget.args.bot); }, child: Icon( DidvanIcons.refresh_solid, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), if (state.messages[mIndex].prompts[index] .text != null || (state.messages[mIndex].prompts[index] .text != null && state.messages[mIndex].prompts[index] .text!.isNotEmpty)) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { await Clipboard.setData(ClipboardData( text: state.messages[mIndex] .prompts[index].text .toString())); Future.delayed( Duration.zero, () => ActionSheetUtils(context) .showAlert(AlertData( message: "متن با موفقیت کپی شد", aLertType: ALertType.success)), ); }, child: Icon( DidvanIcons.copy_regular, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { if (message.id != null) { state.deleteMessage( message.id!, mIndex, index); } else { state.messages[mIndex].prompts .removeAt(index); state.update(); } }, child: Icon( DidvanIcons.trash_solid, size: 18, color: Theme.of(context) .colorScheme .focusedBorder, ), ), ), ], ), ), const SizedBox(height: 8), ], ), ), ) ], ), 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, ), ], ), ], ), ); } Container messageFile( BuildContext context, Prompts message, AiChatState state) { return Container( decoration: BoxDecoration( borderRadius: DesignConfig.mediumBorderRadius, color: Theme.of(context).colorScheme.border, ), padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), margin: const EdgeInsets.fromLTRB(8, 8, 8, 0), child: Row( children: [ const Icon(Icons.file_copy), const SizedBox( width: 12, ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: MediaQuery.sizeOf(context).width, child: MarqueeText( text: message.fileName.toString(), style: const TextStyle(fontSize: 14), stop: const Duration(seconds: 3), ), ), // if (state.file != null && !kIsWeb) // FutureBuilder( // future: state.file!.main.length(), // builder: (context, snapshot) { // if (!snapshot.hasData) { // return const SizedBox(); // } // return DidvanText( // 'File Size ${(snapshot.data! / 1000).round()} KB', // fontSize: 12, // ); // }) ], ), ) ], ), ); } Widget messageImage(FilesModel file) { return GestureDetector( onTap: () => ActionSheetUtils(context) .openInteractiveViewer(context, file.path, !file.isNetwork()), child: file.isNetwork() ? file.path.startsWith('blob:') ? ClipRRect( borderRadius: DesignConfig.lowBorderRadius, child: Image.network(file.path)) : SkeletonImage( pWidth: MediaQuery.sizeOf(context).width / 1, pHeight: MediaQuery.sizeOf(context).height / 6, imageUrl: file.path, ) : ClipRRect( borderRadius: DesignConfig.lowBorderRadius, child: Image.file(file.main)), ); } }