// ignore_for_file: deprecated_member_use_from_same_package, use_build_context_synchronously import 'dart:math'; import 'package:cross_file/cross_file.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:go_router/go_router.dart'; import 'package:hoshan/core/gen/assets.gen.dart'; import 'package:hoshan/core/gen/my_flutter_app_icons.dart'; import 'package:hoshan/core/routes/route_generator.dart'; import 'package:hoshan/core/services/file_manager/download_file_services.dart'; import 'package:hoshan/core/services/file_manager/pick_file_services.dart'; import 'package:hoshan/core/utils/date_time.dart'; import 'package:hoshan/core/utils/strings.dart'; import 'package:hoshan/data/model/ai/chats_history_model.dart'; import 'package:hoshan/data/model/ai/credit_model.dart'; import 'package:hoshan/data/model/ai/messages_model.dart'; import 'package:hoshan/data/model/ai/send_message_model.dart'; import 'package:hoshan/data/model/chat_args.dart'; import 'package:hoshan/data/model/empty_states_enum.dart'; import 'package:hoshan/ui/screens/chat/bloc/messages_bloc.dart'; import 'package:hoshan/ui/screens/chat/bloc/related_questions_bloc.dart'; import 'package:hoshan/ui/screens/chat/cubit/receive_message_cubit.dart'; import 'package:hoshan/ui/screens/library/bloc/chats_history_bloc.dart'; import 'package:hoshan/ui/screens/library/library_screen.dart'; import 'package:hoshan/ui/screens/splash/cubit/user_info_cubit.dart'; import 'package:hoshan/ui/theme/colors.dart'; import 'package:hoshan/ui/theme/cubit/theme_mode_cubit.dart'; import 'package:hoshan/ui/theme/responsive.dart'; import 'package:hoshan/ui/theme/text.dart'; import 'package:hoshan/ui/widgets/components/animations/animated_visibility.dart'; import 'package:hoshan/ui/widgets/components/audio/player.dart'; import 'package:hoshan/ui/widgets/components/audio/recorder.dart'; import 'package:hoshan/ui/widgets/components/button/circle_icon_btn.dart'; import 'package:hoshan/ui/screens/chat/cubit/like_message_cubit.dart'; import 'package:hoshan/ui/widgets/components/dialog/bottom_sheets.dart'; import 'package:hoshan/ui/widgets/components/dialog/dialog_handler.dart'; import 'package:hoshan/ui/widgets/components/dropdown/hint_tooltip.dart'; import 'package:hoshan/ui/widgets/components/dropdown/more_popup_menu.dart'; import 'package:hoshan/ui/widgets/components/image/custome_image.dart'; import 'package:hoshan/ui/widgets/components/image/network_image.dart'; import 'package:hoshan/ui/widgets/components/snackbar/snackbar_manager.dart'; import 'package:hoshan/ui/widgets/components/text/default_markdown_text.dart'; import 'package:hoshan/ui/widgets/components/video/video_thumbnail.dart'; import 'package:hoshan/ui/widgets/sections/empty/empty_states.dart'; import 'package:hoshan/ui/widgets/sections/loading/chat_screen_placeholder.dart'; import 'package:hoshan/ui/widgets/sections/loading/default_placeholder.dart'; class ChatPage extends StatefulWidget { final ChatArgs chatArgs; const ChatPage({super.key, required this.chatArgs}); @override State createState() => _ChatPageState(); } class _ChatPageState extends State { late int? chatId = widget.chatArgs.chatId; late final bot = widget.chatArgs.bot; late final ValueNotifier visibleAttach = ValueNotifier(widget.chatArgs.bot.attachment == 3); final ValueNotifier showRecorder = ValueNotifier(false); final ValueNotifier isGhost = ValueNotifier(false); final ValueNotifier recording = ValueNotifier(false); final ValueNotifier refreshQuestions = ValueNotifier(true); final ValueNotifier webSearch = ValueNotifier(false); final ValueNotifier showInfo = ValueNotifier(false); final ValueNotifier selectedFile = ValueNotifier(null); final TextEditingController messageText = TextEditingController(); ValueNotifier maxLines = ValueNotifier(5); void sendRequest( {required final String? message, final bool retry = false, final XFile? file, final bool withOutNewMessage = false}) { final creditState = CreditModel( credit: UserInfoCubit.userInfoModel.credit ?? 0, freeCredit: UserInfoCubit.userInfoModel.freeCredit ?? 0); int credit = (creditState.freeCredit ?? 0) + (creditState.credit ?? 0); if (credit < widget.chatArgs.bot.cost!) { DialogHandler(context: context).showUpgradeCredit(); messageText.text = message ?? ''; return; } if (!withOutNewMessage) { context.read().add(AddMessage( message: Messages( role: 'human', id: 'hero', content: [ if (message != null && message.isNotEmpty) Content(type: 'text', text: message) ], query: message, file: file, retry: retry))); } context.read().execute( request: SendMessageModel( botId: bot.id, file: file, id: chatId, messageId: 'hero', query: message, ghost: isGhost.value, tool: bot.tool, webSearch: webSearch.value, retry: retry)); if (widget.chatArgs.bot.attachment != 3 && refreshQuestions.value) { context.read().add(ClearAllRelatedQuestions()); } selectedFile.value = null; showRecorder.value = false; messageText.clear(); } void _popUpMenu( Messages message, GlobalKey> containerKey) { String copyText = ''; List urls = []; if (message.content != null && message.content!.isNotEmpty) { for (var content in message.content!) { if (content.imageUrl != null) { if (content.imageUrl!.url != null) { urls.add(content.imageUrl!.url!); } copyText += content.imageUrl!.query ?? ''; copyText += ' '; } if (content.audioUrl != null) { if (content.audioUrl!.url != null) { urls.add(content.audioUrl!.url!); } copyText += content.audioUrl!.query ?? ''; copyText += ' '; } if (content.pdfUrl != null) { if (content.pdfUrl!.url != null) { urls.add(content.pdfUrl!.url!); } copyText += content.pdfUrl!.query ?? ''; copyText += ' '; } if (content.pdfUrl == null && content.imageUrl == null && content.audioUrl == null) { copyText += content.text ?? ''; copyText += ' '; } } } final items = [ PopUpMenuItemModel( popupMenuItem: PopupMenuItem( value: 0, child: MorePopupMenuHandler.morePopUpItem( color: message.fromBot! ? Theme.of(context).colorScheme.primary : Colors.white, icon: Assets.icon.outline.trash, title: 'حذف')), click: () { try { context .read() .add(DeleteMessage(chatId: chatId!, message: message)); } catch (e) { if (kDebugMode) { print("Error when delete message: $e"); } } }, ), if ((bot.deleted != null && !bot.deleted!)) if (!message.fromBot!) PopUpMenuItemModel( popupMenuItem: PopupMenuItem( value: 1, child: MorePopupMenuHandler.morePopUpItem( color: message.fromBot! ? Theme.of(context).colorScheme.primary : Colors.white, icon: Assets.icon.outline.bitcoinRefresh, title: 'دوباره بپرس')), click: () { if (message.content != null && message.content! .firstWhere( (element) => element.type == 'text', ) .text != null) { refreshQuestions.value = false; sendRequest( file: message.file, message: message.content! .firstWhere( (element) => element.type == 'text', ) .text!, retry: true); } }, ), if (copyText.replaceAll(' ', '').isNotEmpty) PopUpMenuItemModel( popupMenuItem: PopupMenuItem( value: 2, child: MorePopupMenuHandler.morePopUpItem( color: message.fromBot! ? Theme.of(context).colorScheme.primary : Colors.white, icon: Assets.icon.outline.copy, title: 'کپی')), click: () async { await Clipboard.setData(ClipboardData(text: copyText)); if (mounted) { SnackBarManager(context, id: 'Copy').show( status: SnackBarStatus.success, message: 'پیام با موفقیت کپی شد 😃', ); } }, ), ]; MorePopupMenuHandler(context: context).showMorePopupMenu( right: message.fromBot!, color: message.error! ? AppColors.red.defaultShade : message.fromBot! ? Theme.of(context).colorScheme.surface : AppColors.primaryColor.defaultShade, containerKey: containerKey, items: [ ...items, if (urls.isNotEmpty) for (var i = 0; i < urls.length; i++) PopUpMenuItemModel( popupMenuItem: PopupMenuItem( value: i + items.length, child: MorePopupMenuHandler.morePopUpItem( color: message.fromBot! ? Theme.of(context).colorScheme.primary : Colors.white, icon: Assets.icon.outline.download, title: 'دانلود ${urls[i].isVideo() ? "ویدیو" : urls[i].isImage() ? 'عکس' : urls[i].isAudio() ? 'فایل صوتی' : 'فایل'}')), click: () async { DownloadFileService.getFile(url: urls[i]).then((value) { SnackBarManager(context).show( message: 'فایل با موفقیت در پوشه Downloads نشست.', status: SnackBarStatus.success); }); }, ), ]); } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().add(RestartChatsHistory()); context.read().add( GetAllChats(type: widget.chatArgs.isPerson ? 'character' : 'llm')); }); } late double maxWidthDesktop; @override Widget build(BuildContext context) { return Theme( data: Theme.of(context).copyWith( bottomSheetTheme: const BottomSheetThemeData( surfaceTintColor: Colors.transparent, backgroundColor: Colors.transparent)), child: Scaffold( drawer: Drawer( shape: const BeveledRectangleBorder(borderRadius: BorderRadius.zero), child: LibraryScreen( type: widget.chatArgs.isPerson ? 'character' : 'llm', onTap: (chat) async { context.push(Routes.chat, extra: ChatArgs( bot: chat.bot!, chatId: chat.id, isPerson: widget.chatArgs.isPerson)); }, ), ), appBar: AppBar( // actions: [ // InkWell( // onTap: () { // DialogHandler(context: context).showPrivateBots(); // }, // child: CircleIconBtn( // size: Responsive(context).isMobile() ? 32 : 46, // iconPadding: const EdgeInsets.all(8), // icon: Assets.icon.outline.crown, // color: context.read().isDark() // ? AppColors.black[900] // : AppColors.secondryColor[50], // iconColor: Theme.of(context).colorScheme.secondary, // ), // ), // const SizedBox( // width: 16, // ), // ], leading: Builder(builder: (context) { return IconButton( icon: const Icon(Icons.menu), onPressed: () { Scaffold.of(context).openDrawer(); }, ); }), ), body: Stack( children: [ SizedBox( width: double.infinity, height: MediaQuery.sizeOf(context).height, child: Assets.image.chatBack.image(fit: BoxFit.cover), ), Responsive(context).maxWidthInDesktop( maxWidth: 800, child: (contxet, maxWidth) { maxWidthDesktop = maxWidth; return SingleChildScrollView( controller: ReceiveMessageCubit.scrollController, reverse: true, physics: context.watch().state is MessagesLoading ? const NeverScrollableScrollPhysics() : const BouncingScrollPhysics(), child: Column( children: [ messages(), aNewMessage(), if (widget.chatArgs.bot.attachment != 3) relatedQuestions(), const SizedBox( height: 120, ), ValueListenableBuilder( valueListenable: selectedFile, builder: (context, value, child) => SizedBox( height: value != null ? 70 : 0, ), ) ], ), ); }), ], ), bottomSheet: messageBar(), ), ); } Widget messages() { return BlocConsumer( listener: (context, state) { if (state is MessagesSuccess) { if (state.isGetAll) { if (chatId != null && widget.chatArgs.bot.attachment != 3 && (widget.chatArgs.bot.tool != null && !widget.chatArgs.bot.tool!)) { try { if (refreshQuestions.value) { context .read() .add(GetAllRelatedQuestions( chatId: chatId!, messageId: state.messages .lastWhere( (element) => element.role == 'human', ) .id!, content: state.messages.last.query ?? '', )); } } catch (e) { if (kDebugMode) { print("Error while get Related Questions is: $e"); } } } } } }, builder: (context, state) { if (state is MessagesFail) { return Padding( padding: EdgeInsets.only(top: MediaQuery.sizeOf(context).height * 0.1), child: EmptyStates.getEmptyState(status: EmptyStatesEnum.server), ); } if (state is MessagesLoading) { return const ChatScreenPlaceholder(); } if (state is MessagesSuccess) { return ListView.builder( shrinkWrap: true, itemCount: state.messages.length, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { final message = state.messages[index]; final GlobalKey containerKey = GlobalKey(); final GlobalKey markdownKey = GlobalKey(); ValueNotifier directionName = ValueNotifier('RTL'); return GestureDetector( onLongPress: () { _popUpMenu(message, containerKey); }, child: Container( alignment: message.fromBot! ? Alignment.centerLeft : Alignment.centerRight, padding: const EdgeInsets.all(16), child: Directionality( textDirection: message.fromBot! ? TextDirection.ltr : TextDirection.rtl, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( constraints: BoxConstraints( minWidth: Responsive(context).isMobile() ? maxWidthDesktop * 0.4 : maxWidthDesktop * 0.2, maxWidth: Responsive(context).isMobile() ? maxWidthDesktop * 0.8 : maxWidthDesktop * 0.6), decoration: BoxDecoration( color: message.error! ? AppColors.red.defaultShade : message.fromBot! ? Theme.of(context) .colorScheme .surface : AppColors.primaryColor.defaultShade, borderRadius: BorderRadius.circular(16) .copyWith( topRight: message.fromBot! ? const Radius.circular(10) : const Radius.circular(0), bottomLeft: message.fromBot! ? const Radius.circular(0) : const Radius.circular(10))), padding: const EdgeInsets.all(8), child: message.content != null ? Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ if (message.file != null) message.file!.name.isVideo() ? GestureDetector( onTap: () => DialogHandler( context: context) .showVideoHero( url: message .file!.path), child: Container( constraints: const BoxConstraints( maxWidth: double.infinity, ), child: VideoThumbnailWidget( videoUrl: message.file!.path), ), ) : message.file!.name.isImage() ? GestureDetector( onTap: () => DialogHandler( context: context) .showImageHero( image: message .file! .path), child: AspectRatio( aspectRatio: 1 / 1, child: Container( constraints: const BoxConstraints( maxWidth: double.infinity, ), child: ClipRRect( borderRadius: BorderRadius .circular( 8), child: CustomeImage( src: message .file!.path, fit: BoxFit .cover, )), ), ), ) : message.file!.name.isAudio() ? Player( fileUrl: message .file!.path, inMessages: true, ) : Container( decoration: BoxDecoration( color: context .read< ThemeModeCubit>() .isDark() ? AppColors .black[ 900] : AppColors .gray .defaultShade, borderRadius: BorderRadius .circular( 10)), padding: const EdgeInsets .all(8), constraints: const BoxConstraints( minHeight: 64), child: Row( children: [ SizedBox( child: message .file! .name .isDocument() ? const Icon( CupertinoIcons .doc) : const SizedBox .shrink(), ), Expanded( child: Padding( padding: const EdgeInsets .symmetric( horizontal: 12.0), child: Text( message.file! .name, textDirection: message .file! .name .startsWithEnglish() ? TextDirection .ltr : TextDirection .rtl, style: const TextStyle( fontSize: 16), overflow: TextOverflow .ellipsis, maxLines: 2, ), )), ], ), ), ...List.generate( message.content!.length, (index) { final content = message.content![index]; return Column( children: [ if (content.audioUrl != null) Player( fileUrl: content.audioUrl!.url ?? '', inMessages: true, ), if (content.imageUrl != null) Container( constraints: const BoxConstraints( maxWidth: double.infinity, ), child: AspectRatio( aspectRatio: 1 / 1, child: ImageNetwork( url: content.imageUrl ?.url ?? '', showHero: true, radius: 10, ), ), ), if (content.pdfUrl != null) Container( decoration: BoxDecoration( color: context .read< ThemeModeCubit>() .isDark() ? AppColors .black[900] : AppColors.gray .defaultShade, borderRadius: BorderRadius .circular(10)), padding: const EdgeInsets.all(8), constraints: const BoxConstraints( minHeight: 64), child: Row( children: [ const SizedBox( child: Icon( CupertinoIcons .doc), ), Expanded( child: Padding( padding: const EdgeInsets .symmetric( horizontal: 12.0), child: Text( content.pdfUrl!.url ?.split('/') .last ?? '', textDirection: (content .pdfUrl! .url ?.split( '/') .last ?? '') .startsWithEnglish() ? TextDirection .ltr : TextDirection .rtl, style: const TextStyle( fontSize: 16), overflow: TextOverflow .ellipsis, maxLines: 2, ), )), ], ), ), if (content.audioUrl == null && content.imageUrl == null && content.pdfUrl == null) Padding( padding: const EdgeInsets .symmetric( horizontal: 8.0), child: Builder( builder: (context) { directionName .value = content .text != null && content.text! .startsWithEnglish() ? "LTR" : 'RTL'; return DefaultMarkdownText( key: markdownKey, text: content.text ?? '', fromBot: message.fromBot!, color: message .fromBot! ? Theme.of( context) .colorScheme .onSurface : Colors.white); }), ) ], ); }, ), if (message.fromBot!) Padding( padding: const EdgeInsets.only( top: 8.0, left: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment .spaceBetween, children: [ ValueListenableBuilder( valueListenable: directionName, builder: (context, dir, _) { return InkWell( onTap: () { directionName .value = markdownKey .currentState ?.changeDirection() ?? 'RTL'; }, child: Container( padding: const EdgeInsets .symmetric( horizontal: 12, vertical: 4), decoration: BoxDecoration( borderRadius: BorderRadius .circular( 4), color: context .read< ThemeModeCubit>() .isDark() ? AppColors .black[ 900] : AppColors .primaryColor[ 50]), child: Text( dir, style: TextStyle( color: Theme.of( context) .colorScheme .primary), ), ), ); }), Row( mainAxisSize: MainAxisSize.min, children: [ BlocProvider< LikeMessageCubit>( create: (context) => LikeMessageCubit() ..getLike( like: message .like), child: BlocBuilder< LikeMessageCubit, LikeMessageState>( builder: (context, state) { return DefaultPlaceHolder( enabled: state is LikeMessageLoading, child: Row( children: [ GestureDetector( onTap: () async { await context.read().setLike( like: state is LikeMessageLiked ? null : true, chatId: chatId!, messageId: message.id!); }, child: Padding( padding: const EdgeInsets .only( right: 20.0), child: SizedBox( width: 16, height: 16, child: state is LikeMessageLiked ? Assets .icon .bold .like .svg(color: AppColors.green.defaultShade) : Assets.icon.outline.like.svg(color: Theme.of(context).colorScheme.primary), ), ), ), GestureDetector( onTap: () async { await context.read().setLike( like: state is LikeMessageDisLiked ? null : false, chatId: chatId!, messageId: message.id!); }, child: Padding( padding: const EdgeInsets .only( right: 20.0), child: SizedBox( width: 16, height: 16, child: state is LikeMessageDisLiked ? Assets .icon .bold .dislike .svg(color: AppColors.red.defaultShade) : Assets.icon.outline.dislike.svg(color: Theme.of(context).colorScheme.primary), ), ), ), ], ), ); }, ), ), if (widget.chatArgs.bot .tool != null && !widget .chatArgs.bot.tool!) MorePopupMenuHandler( context: context) .morePopUpMenu( child: Padding( padding: const EdgeInsets .only( right: 20.0), child: CircleIconBtn( icon: Assets .icon .outline .magicpen, color: context .read< ThemeModeCubit>() .isDark() ? AppColors .black[ 900] : AppColors .primaryColor[50], iconColor: Theme.of( context) .colorScheme .primary, size: 28, iconPadding: const EdgeInsets .all( 6), ), ), items: [ PopUpMenuItemModel( click: () { try { refreshQuestions .value = false; sendRequest( file: message .file, message: 'خلاصه‌تر بنویس'); } catch (e) { if (kDebugMode) { print( 'Error is: $e'); } } }, popupMenuItem: PopupMenuItem( value: 0, child: MorePopupMenuHandler.morePopUpItem( icon: Assets .icon .outline .eraser, title: 'خلاصه‌تر بنویس')), ), PopUpMenuItemModel( click: () { try { refreshQuestions .value = false; sendRequest( file: message .file, message: 'کامل بنویس'); } catch (e) { if (kDebugMode) { print( 'Error is: $e'); } } }, popupMenuItem: PopupMenuItem( value: 1, child: MorePopupMenuHandler.morePopUpItem( icon: Assets .icon .outline .edit2, title: 'کامل بنویس')), ), PopUpMenuItemModel( popupMenuItem: PopupMenuItem( value: 2, child: MorePopupMenuHandler.morePopUpItem( icon: Assets .icon .outline .voiceCricle, title: 'لحن نوشته را تغییر بده')), click: () async { await BottomSheetHandler( context) .showStringList( onSelect: (value) { try { refreshQuestions.value = false; sendRequest( file: message.file, message: 'لحن نوشته را تغییر بده به $value'); } catch (e) { if (kDebugMode) { print('Error is: $e'); } } }, title: 'انتخاب لحن نوشته', values: [ 'رسمی', 'عامیانه', 'دوستانه', 'حرفه ای', 'محاوره ای', 'طنز', 'جدی' ]); }, ), PopUpMenuItemModel( popupMenuItem: PopupMenuItem( value: 3, child: MorePopupMenuHandler.morePopUpItem( icon: Assets .icon .outline .translate, title: 'ترجمه کن')), click: () async { await BottomSheetHandler( context) .showStringList( onSelect: (value) { try { refreshQuestions.value = false; sendRequest( file: message.file, message: 'زبان نوشته را تغییر بده به $value'); } catch (e) { if (kDebugMode) { print('Error is: $e'); } } }, title: 'انتخاب زبان', values: [ '🇮🇷 فارسی', 'Arabic 🇸🇦', 'Bengali 🇧🇩', 'English 🇬🇧', 'French 🇫🇷', 'German 🇩🇪', 'Hindi 🇮🇳', 'Italian 🇮🇹' ]); }, ), ]), ], ), ], ), ) ], ) : null, ), const SizedBox( width: 4, ), message.error! ? CircleIconBtn( key: containerKey, icon: Assets.icon.outline.bitcoinRefresh, iconColor: AppColors.red.defaultShade, iconPadding: const EdgeInsets.all(6), onTap: () async { if (message.query != null) { refreshQuestions.value = true; sendRequest( file: message.file, withOutNewMessage: true, message: message.query!, ); context.read().add( ChangeMessage( oldMessage: message, newMessage: message.copyWith( error: false))); } }, ) : Transform.rotate( angle: pi / 2, child: CircleIconBtn( color: message.fromBot! ? context .read() .isDark() ? AppColors.black[900] : AppColors.primaryColor[50] : AppColors.primaryColor.defaultShade, iconColor: message.fromBot! ? Theme.of(context) .colorScheme .primary : Colors.white, key: containerKey, icon: Assets.icon.outline.more, iconPadding: const EdgeInsets.all(6), onTap: () async { _popUpMenu(message, containerKey); }, ), ) ], ), Padding( padding: const EdgeInsets.all(4.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (message.fromBot!) // Row( // children: [ // ImageNetwork( // url: bot.image ?? '', // width: 16, // height: 16, // radius: 360, // ), // const SizedBox( // width: 4, // ), // Text(bot.name ?? '', // textDirection: TextDirection.rtl, // style: AppTextStyles.body5.copyWith( // fontWeight: FontWeight.bold, // color: Theme.of(context) // .colorScheme // .onSurface)), // const SizedBox( // width: 8, // ), // ], // ), if (message.createdAt != null) Text( DateTimeUtils.convertToSentTime( message.createdAt!), style: AppTextStyles.body5.copyWith( color: Theme.of(context) .colorScheme .onSurface)), ], ), ) ], ), ), ), ); }, ); } return chatScreen(); }, ); } Widget aNewMessage() { return BlocConsumer( builder: (context, state) { if (state is ReceiveMessageOnResponsing) { return Container( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.all(16), child: Container( constraints: BoxConstraints( minWidth: Responsive(context).isMobile() ? maxWidthDesktop * 0.4 : maxWidthDesktop * 0.2, maxWidth: Responsive(context).isMobile() ? maxWidthDesktop * 0.8 : maxWidthDesktop * 0.6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(10) .copyWith(bottomLeft: const Radius.circular(0))), padding: const EdgeInsets.all(16), child: DefaultMarkdownText( text: '${state.text}...', color: Theme.of(context).colorScheme.onSurface, ), ), ), ); } if (state is ReceiveMessageLoading) { return Container( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.all(16), child: Container( width: Responsive(context).isMobile() ? maxWidthDesktop * 0.8 : maxWidthDesktop * 0.6, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(10) .copyWith(bottomLeft: const Radius.circular(0))), padding: const EdgeInsets.all(16), child: Center( child: SpinKitThreeBounce( color: Theme.of(context).colorScheme.primary, size: 32, )), ), ), ); } return const SizedBox.shrink(); }, listener: (context, state) { if (state is ReceiveMessageDone) { context.read().add(AddMessage(message: state.message)); if (state.model.chatId != null) { if (chatId == null && !isGhost.value) { context.read().add(AddChat( chats: Chats( bot: bot, title: state.model.chatTitle, createdAt: DateTime.now().toIso8601String(), id: state.model.chatId))); } chatId = state.model.chatId; try { if (widget.chatArgs.bot.attachment != 3 && (widget.chatArgs.bot.tool != null && !widget.chatArgs.bot.tool! && refreshQuestions.value)) { context.read().add(GetAllRelatedQuestions( chatId: chatId!, messageId: state.message.id!, content: state.message.query!)); } } catch (e) { if (kDebugMode) { print('Error is: $e'); } } } final humanMessage = context.read().state.messages.firstWhere( (element) => element.id == state.oldHumanMessageId, ); if (state.model.humanMessageId != null) { context.read().add(ChangeMessage( oldMessage: humanMessage, newMessage: humanMessage.copyWith(id: state.model.humanMessageId))); } context.read().changeCredit(CreditModel( credit: state.model.credit, freeCredit: state.model.freeCredit)); } else if (state is ReceiveMessageOnFail) { SnackBarManager(context, id: 'ReceiveMessageOnFail').show( status: SnackBarStatus.error, message: state.detail, ); final humanMessage = context.read().state.messages.firstWhere( (element) => element.id == state.oldHumanMessageId, ); context.read().add(ChangeMessage( oldMessage: humanMessage, newMessage: humanMessage.copyWith(error: true))); } }, ); } Widget relatedQuestions() { return BlocBuilder( builder: (context, state) { if (state is RelatedQuestionsSuccess && state.relatedQuestionsModel.questions != null && state.relatedQuestionsModel.questions!.isNotEmpty) { return Directionality( textDirection: TextDirection.rtl, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Column( children: [ Row( children: [ Assets.icon.outline.messageQuestion.svg( color: Theme.of(context).colorScheme.primary), const SizedBox( width: 4, ), Text( 'سوالات مرتبط:', style: AppTextStyles.body3.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary), ), ], ) ], ), const SizedBox( height: 12, ), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: state.relatedQuestionsModel.questions!.length, itemBuilder: (context, index) { final question = state.relatedQuestionsModel.questions![index]; return GestureDetector( onTap: () { refreshQuestions.value = true; sendRequest(message: question); }, child: Directionality( textDirection: question.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0), child: Text( question, textDirection: question.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, style: AppTextStyles.body4.copyWith( color: AppColors.gray[context .read() .isDark() ? 600 : 900]), ), ), if (index != state.relatedQuestionsModel.questions! .length - 1) Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Divider( color: AppColors.gray.defaultShade, ), ) ], ), ), ); }, ), ], ), ), ); } return const SizedBox(); }, ); } Widget chatScreen() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ widget.chatArgs.isPerson ? Column( children: [ const SizedBox( height: 16, ), ImageNetwork( url: widget.chatArgs.bot.image ?? '', width: 120, height: 120, radius: 16, ), const SizedBox( height: 16, ), Text( widget.chatArgs.bot.name ?? '', style: AppTextStyles.headline6.copyWith( color: Theme.of(context).colorScheme.onSurface), ), if (widget.chatArgs.bot.description != null) Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Theme.of(context).colorScheme.surface), child: Center( child: Text( widget.chatArgs.bot.description!, textDirection: TextDirection.rtl, textAlign: TextAlign.justify, style: AppTextStyles.body5.copyWith( color: Theme.of(context).colorScheme.onSurface), ), ), ), ], ) : bot.tool ?? false ? Column( children: [ const SizedBox( height: 24, ), Center( child: ImageNetwork( url: bot.image ?? '', width: 64, height: 64, radius: 360, color: bot.image != null && bot.image!.contains('/llm') ? Theme.of(context).colorScheme.onSurface : null, ), ), const SizedBox( height: 8, ), Text( bot.name ?? '', textDirection: TextDirection.rtl, style: AppTextStyles.headline6.copyWith( color: Theme.of(context).colorScheme.onSurface), ), if (bot.description != null) LayoutBuilder(builder: (context, constraints) { return ValueListenableBuilder( valueListenable: maxLines, builder: (context, lines, _) { final span = TextSpan( text: bot.description, style: AppTextStyles.body4.copyWith( color: AppColors.gray[context .read() .isDark() ? 600 : 900])); final tp = TextPainter( text: span, textDirection: TextDirection.ltr); tp.layout(maxWidth: constraints.maxWidth); final numLines = tp.computeLineMetrics().length; return Padding( padding: const EdgeInsets.fromLTRB(4, 12, 4, 4), child: Stack( children: [ Column( children: [ Text( bot.description!, textDirection: TextDirection.rtl, style: AppTextStyles.body4.copyWith( color: AppColors.gray[context .read< ThemeModeCubit>() .isDark() ? 600 : 900]), maxLines: (lines), textAlign: TextAlign.justify, ), if (lines == null && numLines >= 5) Transform.rotate( angle: -pi / 2, child: CircleIconBtn( onTap: () { if (maxLines.value == null) { maxLines.value = 5; return; } maxLines.value = null; }, size: 46, color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, icon: Assets.icon.outline .arrowRight)), ], ), if (lines != null && numLines > lines) Positioned( bottom: 0, left: 0, right: 0, child: Container( height: 64, decoration: BoxDecoration( gradient: LinearGradient( colors: [ Theme.of(context) .scaffoldBackgroundColor, Theme.of(context) .scaffoldBackgroundColor .withAlpha(140) ], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), ), alignment: Alignment.bottomCenter, child: SizedBox( child: Transform.rotate( angle: pi / 2, child: CircleIconBtn( onTap: () { if (maxLines .value == null) { maxLines.value = 5; return; } maxLines.value = null; }, size: 46, color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, icon: Assets .icon .outline .arrowRight)), ), )) ], ), ); }); }), const SizedBox( height: 8, ), ], ) : Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: Theme.of(context).colorScheme.surface), child: Center( child: Text( 'سلام! به هوشان خوش اومدی. من اینجا هستم تا پاسخگوی سوالاتت باشم. امیدوارم در استفاده از هوشان تجربه خوبی داشته باشی!', textDirection: TextDirection.rtl, textAlign: TextAlign.justify, style: AppTextStyles.body5.copyWith( color: Theme.of(context).colorScheme.onSurface), ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ValueListenableBuilder( valueListenable: isGhost, builder: (context, g, _) { return Transform.scale( scale: 0.8, child: Switch.adaptive( value: g, thumbIcon: WidgetStateProperty.resolveWith( (Set states) { if (states.contains(WidgetState.selected)) { return Icon(CustomIcons.ghost, color: Theme.of(context) .colorScheme .onSurface); } return Icon(Icons.close, color: Theme.of(context) .colorScheme .onSurface); }, ), onChanged: (value) { isGhost.value = value; }, ), ); }), const SizedBox( width: 8, ), Text( 'حالت ناشناس', style: AppTextStyles.body4.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold), ), const SizedBox( width: 8, ), Padding( padding: const EdgeInsets.only(bottom: 4.0), child: HintTooltip( hint: 'با فعال کردن این گزینه؛ چت‌های شما در قسمت تاریخچه، ذخیره نمی‌شوند و اطلاعاتتان ناشناس باقی می‌ماند.', iconColor: Theme.of(context).colorScheme.onSurface, ), ) ], ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondary, borderRadius: BorderRadius.circular(12)), child: Row( children: [ Text( bot.cost == 0 || bot.cost == null ? 'رایگان' : bot.cost.toString(), style: AppTextStyles.body3.copyWith(color: Colors.white), ), const SizedBox( width: 4, ), Assets.icon.outline.coin .svg(color: Colors.white, width: 18, height: 18) ], ), ), ], ), ], ), ); } Widget messageBar() { return Directionality( textDirection: TextDirection.rtl, child: Column( mainAxisSize: MainAxisSize.min, children: [ ValueListenableBuilder( valueListenable: selectedFile, builder: (context, value, child) { if (value != null && !showRecorder.value) { return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(10)), margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(8), constraints: const BoxConstraints(minHeight: 64), child: Row( children: [ SizedBox( child: value.name.isImage() ? GestureDetector( onTap: () => DialogHandler(context: context) .showImageHero(image: value.path), child: SizedBox( width: 46, child: AspectRatio( aspectRatio: 3 / 4, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: CustomeImage( src: value.path, fit: BoxFit.cover, )), ), ), ) : value.path.isDocument() ? const Icon(CupertinoIcons.doc) : value.path.isAudio() ? Player( fileUrl: value.path, inMessages: true, ) : const SizedBox.shrink(), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Text( value.name, textDirection: value.name.startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, style: TextStyle( fontSize: 16, color: Theme.of(context).colorScheme.onSurface), overflow: TextOverflow.ellipsis, maxLines: 2, ), )), CircleIconBtn( icon: Assets.icon.outline.trash, color: AppColors.red[50], iconColor: AppColors.red.defaultShade, iconPadding: const EdgeInsets.all(6), onTap: () { selectedFile.value = null; }, ) ], ), ); } return const SizedBox.shrink(); }, ), Container( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), margin: Responsive(context).isMobile() ? EdgeInsets.zero : const EdgeInsets.all(16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.only( topLeft: Radius.circular(32), topRight: Radius.circular(32)), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), offset: const Offset(0, 0), blurRadius: 12, spreadRadius: 0, ), ], ), child: Column( children: [ ValueListenableBuilder( valueListenable: showRecorder, builder: (context, inRecord, child) { return inRecord ? Recorder( play: true, onDelete: () { selectedFile.value = null; showRecorder.value = false; }, onError: (e) { showRecorder.value = false; recording.value = false; }, onRecordFinish: (file) { selectedFile.value = file; recording.value = false; }, ) : Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: widget.chatArgs.bot.attachment == 3 ? const SizedBox.shrink() : SizedBox( child: ValueListenableBuilder( valueListenable: selectedFile, builder: (context, file, child) { return ValueListenableBuilder( valueListenable: messageText, builder: (context, text, child) { return Directionality( textDirection: text.text .startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, child: TextField( controller: messageText, onChanged: (value) {}, enabled: (bot.deleted != null && !bot .deleted!) && (context .watch< ReceiveMessageCubit>() .state is! ReceiveMessageOnResponsing && context .watch< ReceiveMessageCubit>() .state is! ReceiveMessageLoading) && !(bot.attachment == 1 && file != null) && (widget.chatArgs.bot .attachment != 3), minLines: 1, maxLines: 6, // Set this keyboardType: TextInputType .multiline, style: AppTextStyles .body4 .copyWith( color: Theme.of( context) .colorScheme .onSurface), decoration: InputDecoration( contentPadding: const EdgeInsets .fromLTRB( 0, 12, 0, 12), filled: true, hintText: (bot.deleted != null && bot.deleted!) ? 'دستیار مورد نظر توسط سازنده حذف شده است!' : 'چیزی بنویسید ...', hintStyle: AppTextStyles .body4, fillColor: Colors .transparent, border: const OutlineInputBorder( borderSide: BorderSide.none, ), ), ), ); }); }, ), ), ), const SizedBox( width: 16, ), ], ); }), Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ if ((bot.deleted != null && !bot.deleted!)) ValueListenableBuilder( valueListenable: selectedFile, builder: (context, file, child) { return ValueListenableBuilder( valueListenable: recording, builder: (context, inRecording, child) { return ValueListenableBuilder( valueListenable: showRecorder, builder: (context, showRecord, child) { return ValueListenableBuilder( valueListenable: messageText, builder: (context, message, child) { return inRecording ? const SizedBox .shrink() : bot.attachmentType != null && bot.attachmentType! .contains( 'audio') && message.text .replaceAll( ' ', '') .isEmpty && !showRecord && file == null ? GestureDetector( onTap: () { showRecorder .value = true; recording .value = true; }, child: Assets .icon .outline .microphoneChat .svg( width: 24, height: 24, color: Theme.of(context) .colorScheme .primary)) : GestureDetector( onTap: () { if ((messageText .text .replaceAll(' ', '') .isEmpty && widget.chatArgs.bot.attachment != 3) && selectedFile .value == null) { return; } if (selectedFile .value != null && bot.attachment == 1) { return; } refreshQuestions .value = true; sendRequest( file: selectedFile .value, message: messageText .text); }, child: Assets .icon .bold .send .svg( width: 24, height: 24)); }, ); }); }); }), const SizedBox( width: 8, ), // CircleIconBtn( // icon: Assets.icon.outline.infoCircle, // color: Theme.of(context).colorScheme.surface, // iconColor: Theme.of(context).colorScheme.onSurface, // onTap: () { // showInfo.value = !showInfo.value; // }, // ), ], ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ ValueListenableBuilder( valueListenable: webSearch, builder: (context, isWebSearchEnabled, _) { return GestureDetector( onTap: () { webSearch.value = !webSearch.value; }, child: HintTooltip( hint: 'جستجو در وب', child: Assets.icon.outline.globalSearch.svg( width: 24, height: 24, color: isWebSearchEnabled ? Theme.of(context) .colorScheme .primary : Theme.of(context) .colorScheme .onSurface .withOpacity(0.4), ), ), ); }, ), // ValueListenableBuilder( // valueListenable: webSearch, // builder: (context, canWebSearch, _) { // return ChoiceChip( // padding: EdgeInsets.zero, // showCheckmark: false, // labelPadding: // const EdgeInsets.symmetric(horizontal: 8), // selectedColor: Theme.of(context) // .colorScheme // .primary, // Change selected color // backgroundColor: // Theme.of(context).colorScheme.surface, // surfaceTintColor: Colors.transparent, // selectedShadowColor: Colors.transparent, // shape: RoundedRectangleBorder( // borderRadius: BorderRadius.circular(360)), // onSelected: (value) { // webSearch.value = value; // }, // label: Row( // children: [ // Assets.icon.outline.globalSearch.svg( // color: canWebSearch // ? Colors.white // : Theme.of(context) // .colorScheme // .onSurface), // const SizedBox( // width: 4, // ), // Text( // 'جستجو در وب', // style: AppTextStyles.body5.copyWith( // color: canWebSearch // ? Colors.white // : Theme.of(context) // .colorScheme // .onSurface), // ), // ], // ), // selected: canWebSearch); // }), const SizedBox( width: 12, ), if ((bot.deleted != null && !bot.deleted!)) if (bot.attachmentType != null && bot.attachmentType!.isNotEmpty) ValueListenableBuilder( valueListenable: visibleAttach, builder: (context, value, child) { return AnimatedVisibility( isVisible: value, duration: const Duration(milliseconds: 300), fadeMode: FadeMode.horizontal, child: Padding( padding: const EdgeInsets.only(left: 12.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (bot.attachmentType! .contains('image')) Padding( padding: const EdgeInsets.only( right: 8.0), child: CircleIconBtn( icon: Assets.icon.outline .galleryAdd, color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, onTap: () async { await BottomSheetHandler( context) .showPickImage( onSelect: (file) { selectedFile.value = file; if (widget .chatArgs .bot .attachment != 3) { visibleAttach .value = false; } }, ); }), ), if (bot.attachmentType! .contains('audio')) Padding( padding: const EdgeInsets.only( right: 8.0), child: CircleIconBtn( icon: Assets .icon.outline.musicnote, color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, onTap: () async { final file = await PickFileService( context) .getFile( fileType: FileType .audio); if (file != null) { selectedFile.value = file.single; if (widget.chatArgs.bot .attachment != 3) { visibleAttach.value = false; } } }, ), ), if (bot.attachmentType! .contains('pdf')) Padding( padding: const EdgeInsets.only( right: 8.0), child: CircleIconBtn( icon: Assets .icon.outline.cardAdd, color: Theme.of(context) .colorScheme .primary, iconColor: Colors.white, onTap: () async { final file = await PickFileService( context) .getFile( fileType: FileType .custom, allowedExtensions: [ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'xlsm', 'xlsb', 'xlt', 'xltx', 'xltm' ]); if (file != null) { selectedFile.value = file.single; if (widget .chatArgs .bot .attachment != 3) { visibleAttach .value = false; } } }), ) ], ), )); }, ), if ((bot.deleted != null && !bot.deleted!)) if (bot.attachment != 0 && bot.attachmentType != null && bot.attachmentType!.isNotEmpty && bot.attachment != 3) GestureDetector( onTap: () { if (widget.chatArgs.bot.attachment != 3) { visibleAttach.value = !visibleAttach.value; } }, child: Assets.icon.outline.elementPlus.svg( width: 24, height: 24, color: Theme.of(context) .colorScheme .primary)), ], ), ], ), SizedBox( height: 10, ), ], ), const SizedBox( height: 4, ), ValueListenableBuilder( valueListenable: showInfo, builder: (context, show, _) { if (show) { return Text( 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.', style: AppTextStyles.body6.copyWith( color: Theme.of(context).colorScheme.onSurface), ); } return const SizedBox.shrink(); }) ], ), ), ], ), ); } }