// ignore_for_file: library_private_types_in_public_api, deprecated_member_use, depend_on_referenced_packages import 'dart:io'; 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/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/utils/extension.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/ai/widgets/hoshan_drawer.dart'; import 'package:didvan/views/widgets/didvan/didvan_markdown.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/hoshan_app_bar.dart'; import 'package:didvan/views/widgets/marquee_text.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:didvan/views/widgets/video/chat_video_player.dart'; import 'package:didvan/views/widgets/video/custome_controls.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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 { final GlobalKey scaffKey = GlobalKey(); 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, widget.args.assistantsName != null); } }); 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: HoshanAppBar( onBack: () { Navigator.pop(context); }, withActions: false, ), key: scaffKey, resizeToAvoidBottomInset: true, drawer: HoshanDrawer( scaffKey: scaffKey, ), body: state.loading ? Center( child: Image.asset( Assets.loadingAnimation, width: 60, height: 60, ), ) : Stack( children: [ SingleChildScrollView( reverse: true, controller: state.scrollController, child: Column( children: [ Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox( height: 24, ), SkeletonImage( width: 75, height: 75, imageUrl: widget.args.bot.image.toString(), borderRadius: BorderRadius.circular(360), ), const SizedBox( height: 12, ), DidvanText( widget.args.assistantsName ?? widget.args.bot.name.toString(), fontSize: 17, fontWeight: FontWeight.bold, ), if (state.messages.isEmpty) Column( children: [ const SizedBox( height: 16, ), Padding( padding: const EdgeInsets.symmetric( horizontal: 20.0), child: Center( child: DidvanText( widget.args.bot.description ?? '', fontSize: 12, color: Theme.of(context) .colorScheme .caption, textAlign: TextAlign.justify, )), ), const SizedBox( height: 100, ), ], ) ], ), if (state.messages.isNotEmpty) 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); }, ), ], ); }), ], ), ), Positioned( top: 32, right: 0, child: InkWell( onTap: () => scaffKey.currentState!.openDrawer(), child: Container( width: 46, height: 46, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), bottomLeft: Radius.circular(12)), boxShadow: DesignConfig.defaultShadow), child: Icon( DidvanIcons.angle_left_light, color: Theme.of(context).colorScheme.title, ), )), ) ], ), bottomSheet: Column( mainAxisSize: MainAxisSize.min, children: [ // Platform.isIOS // ? AiMessageBarIOS( // bot: widget.args.bot, // ) // : AiMessageBar( bot: widget.args.bot, attch: widget.args.attach, assistantsName: widget.args.assistantsName, ), ], )), ), ); } 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().replaceAll(' ', ''), duration: message.duration != null ? Duration(seconds: message.duration!) : null)); 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 Padding( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16), child: Directionality( textDirection: snapshot.data .toString() .startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, child: DidvanMarkdownText( text: "${snapshot.data}...", ), ), ); }, ); }), 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.isVideo() ? Padding( padding: const EdgeInsets.fromLTRB( 16, 16, 16, 0), child: ClipRRect( borderRadius: DesignConfig.lowBorderRadius, child: ChatVideoPlayer( src: RequestHelper.baseUrl + file.path, custome: const CustomControls(), )), ) : 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!)))) Padding( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16), child: Directionality( textDirection: message.text .toString() .startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, child: DidvanMarkdownText( text: message.text.toString(), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (message.role .toString() .contains('user') && widget.args.assistantsName == null) PopupMenuButton( offset: const Offset(0, 46), onSelected: (value) async { navigatorKey.currentState!.pushNamed( Routes.aiChat, arguments: AiChatArgs( bot: value, prompts: message, isTool: widget.args.isTool ?? context .read< HistoryAiChatState>() .bots)); }, itemBuilder: (BuildContext context) { final bots = widget.args.isTool ?? context .read() .bots; return [ ...List.generate( bots.length, (index) => PopupMenuItem( value: bots[index], height: 72, child: Container( constraints: const BoxConstraints( maxWidth: 200), child: Row( children: [ SkeletonImage( imageUrl: bots[index] .image .toString(), width: 42, height: 42, borderRadius: BorderRadius .circular(360), ), const SizedBox(width: 12), Expanded( child: Directionality( textDirection: TextDirection.ltr, child: DidvanText( 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.assistantsName ?? 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 && !Platform.isIOS) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( onTap: () async { final url = '${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}'; kIsWeb ? MediaService .downloadFileFromWeb(url) : await MediaService.downloadFile( url, name: message.fileName); }, 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, widget.args.assistantsName != null); }, 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) { final String fileName = message.fileName ?? message.fileLocal?.name ?? ''; 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: fileName, style: const TextStyle(fontSize: 14), stop: const Duration(seconds: 3), textDirection: fileName.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, ), ), // 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)), ); } }