import 'dart:async'; 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/models/ai/bots_model.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/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.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/message_bar_btn.dart'; import 'package:didvan/views/ai/widgets/voice_message_view.dart'; import 'package:didvan/views/widgets/animated_visibility.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/marquee_text.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:get/get.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; import 'package:path_provider/path_provider.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; import 'package:record/record.dart'; import 'package:path/path.dart' as p; import 'package:voice_message_package/voice_message_package.dart'; class AiMessageBar extends StatefulWidget { final FocusNode? focusNode; final BotsModel bot; const AiMessageBar({ super.key, this.focusNode, required this.bot, }); @override State createState() => _AiMessageBarState(); static PopupMenuItem popUpBtns({ required final String value, required final IconData icon, final Color? color, final double? height, final double? size, }) { return PopupMenuItem( value: value, height: height ?? 46, child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, color: color, size: size, ), const SizedBox( width: 12, ), DidvanText( value, color: color, fontSize: size, ), ], ), ); } } class _AiMessageBarState extends State { final ValueNotifier messageText = ValueNotifier(''); @override void initState() { widget.focusNode?.addListener(() {}); super.initState(); } final record = AudioRecorder(); @override void dispose() { super.dispose(); record.dispose(); try { _timer.cancel(); } catch (e) { e.printError(); } } late Timer _timer; final ValueNotifier _countTimer = ValueNotifier(0); void startTimer() { const oneSec = Duration(seconds: 1); _timer = Timer.periodic( oneSec, (Timer timer) { _countTimer.value++; }, ); } @override Widget build(BuildContext context) { return Consumer( builder: (context, state, child) { final historyState = context.read(); return IgnorePointer( ignoring: state.onResponsing, child: Column( children: [ fileContainer(), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: Column( children: [ audioContainer(), Container( decoration: BoxDecoration( boxShadow: DesignConfig.defaultShadow, color: Theme.of(context).colorScheme.surface, border: Border.all( color: Theme.of(context).colorScheme.border), borderRadius: BorderRadius.circular(360)), child: Row( children: [ const SizedBox( width: 8, ), Expanded( child: StreamBuilder( stream: record.onStateChanged(), builder: (context, snapshot) { return ValueListenableBuilder( valueListenable: messageText, builder: (context, value, child) { return Row( children: [ (snapshot.hasData && snapshot.data! != RecordState.stop) ? MessageBarBtn( enable: true, icon: DidvanIcons .stop_circle_solid, click: () async { final path = await record.stop(); state.file = FilesModel( path.toString()); _timer.cancel(); _countTimer.value = 0; state.update(); }, ) : widget.bot.attachmentType! .contains( 'audio') && value.isEmpty && state.file == null && widget.bot .attachment != 0 ? MessageBarBtn( enable: false, icon: DidvanIcons .mic_regular, click: () async { if (await record .hasPermission()) { Directory? downloadDir = await getDownloadsDirectory(); record.start( const RecordConfig(), path: '${downloadDir!.path}/${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a'); startTimer(); } }, ) : MessageBarBtn( enable: value.isNotEmpty, icon: DidvanIcons .send_light, click: () async { if (value.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( error: false, text: state .message .text, file: state .file?.path, fileName: state .file == null ? null : 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( error: false, text: state .message .text, finished: true, file: state .file ?.path, fileName: state.file == null ? null : p.basename(state .file! .path), role: 'user', createdAt: DateTime .now() .subtract( const Duration(minutes: 210)) .toIso8601String(), ) ])); } state.message .clear(); messageText.value = state.message .text; await state .postMessage( widget.bot); }, ), const SizedBox( width: 12, ), Expanded( child: Padding( padding: const EdgeInsets .symmetric( horizontal: 8.0, ), child: snapshot.hasData && (snapshot.data! == RecordState .record || snapshot.data! == RecordState .pause) ? Padding( padding: const EdgeInsets .all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment .center, children: [ SpinKitWave( color: Theme.of( context) .colorScheme .primary, size: 32, ), const SizedBox( width: 24, ), ValueListenableBuilder< int>( valueListenable: _countTimer, builder: (context, value, child) => DidvanText(DateTimeUtils.normalizeTimeDuration(Duration( seconds: value))), ) ], ), ) : Form( child: TextFormField( textInputAction: TextInputAction .newline, style: Theme.of(context) .textTheme .bodyMedium, maxLines: 2, minLines: 1, // keyboardType: TextInputType.text, controller: state.message, focusNode: widget.focusNode, enabled: !(state .file != null && widget.bot .attachment == 1), decoration: InputDecoration( border: InputBorder .none, hintText: 'بنویسید...', hintStyle: Theme.of( context) .textTheme .bodySmall! .copyWith( color: Theme.of( context) .colorScheme .disabledText), suffixIcon: state .isEdite ? InkWell( onTap: () { state.isEdite = false; state .update(); }, child: const Icon( DidvanIcons .close_circle_solid), ) : const SizedBox(), ), onChanged: (value) { messageText.value = value; }, )), ), ), if (snapshot.hasData) snapshot.data! == RecordState.record ? MessageBarBtn( enable: false, icon: DidvanIcons .pause_solid, click: () async { await record.pause(); _timer.cancel(); }, ) : snapshot.data! == RecordState.pause ? MessageBarBtn( enable: false, icon: DidvanIcons .play_solid, click: () async { await record .resume(); startTimer(); }, ) : const SizedBox(), const SizedBox( width: 8, ), ], ); }); }), ), const SizedBox( width: 8, ), ], ), ), ], )), const SizedBox( width: 12, ), ValueListenableBuilder( valueListenable: messageText, builder: (context, value, child) { if (context .read() .bot! .attachmentType! .isNotEmpty && (widget.bot.attachment != 0 && (widget.bot.attachment == 1 && value.isEmpty)) || widget.bot.attachment == 2) { return SizedBox( width: 46, height: 46, child: Center( child: PopupMenuButton( onOpened: () {}, surfaceTintColor: Colors.transparent, onSelected: (value) async { switch (value) { case 'Pdf': FilePickerResult? result = await MediaService.pickPdfFile(); if (result != null) { state.file = FilesModel( result.files.single.path!); // 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 ? FilesModel(pickedFile.path) : FilesModel(file!.path); break; case 'Audio': FilePickerResult? result = await MediaService.pickAudioFile(); if (result != null) { state.file = FilesModel( result.files.single.path!); } break; default: } state.update(); }, itemBuilder: (BuildContext context) => [ if (historyState.bot!.attachmentType! .contains('pdf')) AiMessageBar.popUpBtns( value: 'Pdf', icon: Icons.picture_as_pdf), if (historyState.bot!.attachmentType! .contains('image')) AiMessageBar.popUpBtns( value: 'Image', icon: Icons.image), if (historyState.bot!.attachmentType! .contains('audio')) AiMessageBar.popUpBtns( value: 'Audio', icon: Icons.audio_file), ], offset: Offset(-20, widget.focusNode!.hasFocus ? -999 : 999), position: PopupMenuPosition.over, useRootNavigator: true, child: Icon( Icons.attach_file_rounded, color: Theme.of(context) .colorScheme .focusedBorder, ), ), )); } return const SizedBox(); }), ], ), ], ), ); }, ); } AnimatedVisibility audioContainer() { final state = context.watch(); return AnimatedVisibility( isVisible: state.file != null && lookupMimeType(state.file!.path)!.startsWith('audio/'), duration: DesignConfig.lowAnimationDuration, child: SizedBox( width: MediaQuery.sizeOf(context).width, child: Padding( padding: const EdgeInsets.fromLTRB(0, 0, 0, 8), child: Container( height: 46, decoration: BoxDecoration( boxShadow: DesignConfig.defaultShadow, color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(360)), padding: const EdgeInsets.symmetric(horizontal: 20), child: state.file == null ? const SizedBox() : MyVoiceMessageView( size: 32, controller: VoiceController( audioSrc: state.file!.path.startsWith('/uploads') ? '${RequestHelper.baseUrl + state.file!.path}?accessToken=${RequestService.token}' : state.file!.path, onComplete: () { /// do something on complete }, onPause: () { /// do something on pause }, onPlaying: () { /// do something on playing }, onError: (err) { /// do somethin on error }, isFile: !state.file!.path.startsWith('/uploads'), maxDuration: const Duration(seconds: 10), ), innerPadding: 0, cornerRadius: 20, circlesColor: Theme.of(context).colorScheme.primary, activeSliderColor: Theme.of(context).colorScheme.primary, trashClick: () async { state.file = null; state.update(); }, ), ), ), ), ); } AnimatedVisibility fileContainer() { final state = context.watch(); String basename = ''; if (state.file != null) { basename = p.basename(state.file!.path); } return AnimatedVisibility( isVisible: state.file != null && !lookupMimeType(state.file!.path)!.startsWith('audio/'), 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: const EdgeInsets.only(bottom: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Icon(Icons.file_copy), const SizedBox( width: 12, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 260, height: 24, child: MarqueeText( text: basename, style: const TextStyle(fontSize: 14), stop: const Duration(seconds: 3), ), ), if (state.file != null) 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, ); }) ], ) ], ), InkWell( onTap: () { state.file = null; state.update(); }, child: const Icon(DidvanIcons.close_circle_solid)) ], ), ), ); } }