// ignore_for_file: library_private_types_in_public_api, avoid_web_libraries_in_flutter, deprecated_member_use import 'dart:async'; import 'package:didvan/utils/extension.dart'; import 'package:record/record.dart'; import 'package:universal_html/html.dart' as html; import 'dart:io'; import 'package:audio_session/audio_session.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/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/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/audio_wave.dart'; import 'package:didvan/views/ai/widgets/message_bar_btn.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/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_sound/flutter_sound.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_sound_platform_interface/flutter_sound_recorder_platform_interface.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; typedef _Fn = void Function(); class AiMessageBar extends StatefulWidget { final BotsModel bot; final String? assistantsName; final bool? attch; const AiMessageBar( {Key? key, required this.bot, this.attch, this.assistantsName}) : super(key: key); @override _AiMessageBarState createState() => _AiMessageBarState(); } class _AiMessageBarState extends State { FlutterSoundPlayer? _mPlayer = FlutterSoundPlayer(); FlutterSoundRecorder? _mRecorder = FlutterSoundRecorder(); Codec _codec = Codec.aacMP4; String _mPath = '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.mp4'; bool _mPlayerIsInited = false; bool _mRecorderIsInited = false; bool _mplaybackReady = false; late bool openAttach = widget.attch ?? false; Timer? _timer; final theSource = AudioSource.microphone; final ValueNotifier _countTimer = ValueNotifier(Duration.zero); late HistoryAiChatState historyState = context.read(); @override void initState() { _mPlayer!.openPlayer().then((value) { setState(() { _mPlayerIsInited = true; }); }); openTheRecorder().then((value) { setState(() { _mRecorderIsInited = true; }); }); super.initState(); } @override void dispose() { _mPlayer!.closePlayer(); _mPlayer = null; _mRecorder!.closeRecorder(); _mRecorder = null; _timer?.cancel(); super.dispose(); } Future openTheRecorder() async { if (!kIsWeb) { var status = await Permission.microphone.status; await AudioRecorder().hasPermission(); if (status != PermissionStatus.granted) { if (!Platform.isIOS) { throw RecordingPermissionException( 'Microphone permission not granted'); } } } await _mRecorder!.openRecorder(); if (!await _mRecorder!.isEncoderSupported(_codec) && kIsWeb) { _codec = Codec.opusWebM; _mPath = '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.webm'; if (!await _mRecorder!.isEncoderSupported(_codec) && kIsWeb) { _mRecorderIsInited = true; return; } } final session = await AudioSession.instance; await session.configure(AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | AVAudioSessionCategoryOptions.defaultToSpeaker, avAudioSessionMode: AVAudioSessionMode.spokenAudio, avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, androidAudioAttributes: const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, flags: AndroidAudioFlags.none, usage: AndroidAudioUsage.voiceCommunication, ), androidAudioFocusGainType: AndroidAudioFocusGainType.gain, androidWillPauseWhenDucked: true, )); _mRecorderIsInited = true; } void startTimer() { const oneSec = Duration(seconds: 1); _timer = Timer.periodic( oneSec, (Timer timer) { _countTimer.value = Duration(seconds: _countTimer.value.inSeconds + 1); }, ); } void record() async { _countTimer.value = Duration.zero; await _mRecorder! .startRecorder( toFile: _mPath, codec: _codec, audioSource: theSource, ) .then((value) { setState(() { startTimer(); }); }); } void stopRecorder() async { await _mRecorder!.stopRecorder().then((value) { setState(() { var url = value; final state = context.read(); state.file = FilesModel(url!, audio: true, isRecorded: true, duration: _countTimer.value); _mplaybackReady = true; _timer?.cancel(); }); }); _mPlayer!.setSubscriptionDuration(_countTimer.value); } void play() async { assert(_mPlayerIsInited && _mplaybackReady && _mRecorder!.isStopped && _mPlayer!.isStopped); _mPlayer! .startPlayer( fromURI: _mPath, whenFinished: () { setState(() {}); }) .then((value) { setState(() {}); }); } void stopPlayer() { _mPlayer!.stopPlayer().then((value) { setState(() {}); }); } void pausePlayer() { _mPlayer!.pausePlayer().then((value) { setState(() {}); }); } void resumePlayer() { _mPlayer!.resumePlayer().then((value) { setState(() {}); }); } _Fn? getRecorderFn() { if (!_mRecorderIsInited || !_mPlayer!.isStopped) { return null; } return _mRecorder!.isStopped ? record : stopRecorder; } _Fn? getPlaybackFn() { if (!_mPlayerIsInited || !_mplaybackReady || !_mRecorder!.isStopped) { return null; } return _mPlayer!.isPlaying ? pausePlayer : _mPlayer!.isPaused ? resumePlayer : _mPlayer!.isStopped ? play : stopPlayer; } @override Widget build(BuildContext context) { return Consumer(builder: (context, state, child) { return Container( padding: const EdgeInsets.fromLTRB(12, 24, 12, 0).copyWith( top: (state.file != null && !state.file!.isRecorded) ? 0 : 24), decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, ), child: Column( children: [ if (state.file != null && !(state.file!.isRecorded)) fileContainer(), Stack( children: [ Container( decoration: BoxDecoration( boxShadow: DesignConfig.defaultShadow, color: Theme.of(context).colorScheme.surface, border: Border.all( color: Theme.of(context).colorScheme.border), borderRadius: DesignConfig.highBorderRadius), child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ recorderAndSendButton(state, historyState), if (!(_mRecorder!.isStopped)) ValueListenableBuilder( valueListenable: _countTimer, builder: (context, value, child) => Padding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), child: SizedBox( width: 50, child: Center( child: DidvanText( DateTimeUtils.normalizeTimeDuration(value)), ), ), ), ), Expanded( child: Padding( padding: _mRecorder!.isPaused || _mRecorder!.isRecording ? const EdgeInsets.fromLTRB(12, 8, 0, 8) : EdgeInsets.zero, child: recorderAndTextMessageHandler(context, state), ), ), ], ), ), if (state.onResponsing) Positioned.fill( child: Container( decoration: BoxDecoration( color: Theme.of(context) .colorScheme .focused .withOpacity(0.5)), ), ) ], ), MediaQuery.of(context).viewInsets.bottom == 0 ? const Padding( padding: EdgeInsets.fromLTRB(8, 8, 8, 4), child: DidvanText( 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید.', fontSize: 12, ), ) : const SizedBox( height: 12, ) ], ), ); }); } Row recorderAndTextMessageHandler(BuildContext context, AiChatState state) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: _mRecorder!.isPaused ? [ Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( 32, (index) => Padding( padding: const EdgeInsets.symmetric( horizontal: 1.0, vertical: 12), child: Container( width: 3, height: 8, decoration: BoxDecoration( color: Theme.of(context) .colorScheme .primary .withOpacity(0.4)), ), )), ), ), MessageBarBtn( enable: true, icon: DidvanIcons.play_regular, click: () async { await _mRecorder?.resumeRecorder(); setState(() { startTimer(); }); }, ) ] : _mRecorder!.isRecording ? [ Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( 5, (index) => SpinKitWave( color: Theme.of(context) .colorScheme .primary .withOpacity(0.4), size: 32, itemCount: 10, )))), MessageBarBtn( enable: true, icon: DidvanIcons.pause_regular, click: () async { await _mRecorder?.pauseRecorder(); setState(() { _timer!.cancel(); }); }, ) ] : [ Expanded( child: state.file != null && state.file!.isRecorded ? audioContainer() : Directionality( textDirection: state.message.text .toString() .startsWithEnglish() ? TextDirection.ltr : TextDirection.rtl, child: TextFormField( textInputAction: TextInputAction.newline, style: Theme.of(context).textTheme.bodyMedium, minLines: 1, maxLines: 6, // Set this keyboardType: TextInputType.multiline, controller: state.message, enabled: !(state.file != null && widget.bot.attachment == 1), decoration: InputDecoration( contentPadding: const EdgeInsets.fromLTRB(12, 12, 12, 12), 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) { if (value.isEmpty || value.length == 1) { state.update(); } }, ), ), ), Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row(children: [ attachmentLayout(state, context), if (widget.bot.attachmentType!.isNotEmpty) Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), child: MessageBarBtn( enable: false, icon: openAttach || state.file != null ? DidvanIcons.close_regular : Icons.attach_file_outlined, click: () { if (_mPlayer!.isPlaying) { stopPlayer(); } if (state.file != null) { state.file = null; } else { openAttach = !openAttach; } state.update(); }, ), ) ]), ) ], ); } Padding recorderAndSendButton( AiChatState state, HistoryAiChatState historyState) { return Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 8), child: state.message.text.isEmpty && historyState.bot!.attachmentType!.contains('audio') && state.file == null && widget.bot.attachment != 0 ? MessageBarBtn( enable: true, icon: _mRecorder!.isRecording || _mRecorder!.isPaused ? Icons.stop_rounded : DidvanIcons.mic_regular, click: getRecorderFn(), ) : MessageBarBtn( enable: (state.file != null && state.file!.isRecorded) || (widget.bot.attachment == 1) || state.message.text.isNotEmpty, icon: DidvanIcons.send_light, click: () async { if ((state.file == null || !state.file!.isRecorded) && (widget.bot.attachment != 1) && state.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( error: false, text: state.message.text, // file: state.file?.path, // fileName: state.file?.basename, fileLocal: state.file, 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?.basename, fileLocal: state.file, role: 'user', createdAt: DateTime.now() .subtract(const Duration(minutes: 210)) .toIso8601String(), ) ])); } state.message.clear(); openAttach = false; state.update(); await state.postMessage( widget.bot, widget.assistantsName != null); }, ), ); } // AnimatedVisibility attachmentLayout(AiChatState state, // HistoryAiChatState historyState, BuildContext context) { // return AnimatedVisibility( // isVisible: openAttach, // duration: DesignConfig.lowAnimationDuration, // child: Container( // padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 12), // child: state.file != null && !(state.file!.isRecorded) // ? fileContainer() // : Row( // mainAxisAlignment: MainAxisAlignment.center, // children: [ // if (historyState.bot!.attachmentType!.contains('pdf')) // attachBtn( // title: "PDF", // icon: CupertinoIcons.doc_fill, // color: Colors.redAccent, // click: () async { // MediaService.onLoadingPickFile(context); // FilePickerResult? result = // await MediaService.pickPdfFile(); // if (result != null) { // if (kIsWeb) { // Uint8List? bytes = result.files.first // .bytes; // Access the bytes property // String? name = result.files.first.name; // // Store bytes and file name directly in your state or model // state.file = FilesModel( // '', // No need for a file path on web // name: name, // bytes: bytes, // audio: false, // image: false, // ); // } else { // state.file = FilesModel(result.files.single.path!, // audio: false, image: false); // } // } // Future.delayed( // Duration.zero, // () => ActionSheetUtils(context).pop(), // ); // }, // ), // if (historyState.bot!.attachmentType!.contains('image')) // attachBtn( // title: "تصویر", // icon: CupertinoIcons.photo, // color: Colors.deepOrangeAccent, // click: () async { // MediaService.onLoadingPickFile(context); // 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) { // await Future.delayed( // Duration.zero, // () => ActionSheetUtils(context).pop(), // ); // return; // } // } // if (pickedFile == null) { // await Future.delayed( // Duration.zero, // () => ActionSheetUtils(context).pop(), // ); // return; // } // state.file = kIsWeb // ? FilesModel(pickedFile.path, // name: pickedFile.name, // image: true, // audio: false) // : FilesModel(file!.path, // image: true, audio: false); // await Future.delayed( // Duration.zero, // () => ActionSheetUtils(context).pop(), // ); // }, // ), // // if (!kIsWeb && !Platform.isIOS) // if (historyState.bot!.attachmentType!.contains('audio')) // attachBtn( // title: "صوت", // icon: CupertinoIcons.music_note_2, // color: Colors.indigoAccent, // click: () async { // MediaService.onLoadingPickFile(context); // FilePickerResult? result = // await MediaService.pickAudioFile(); // if (result != null) { // if (kIsWeb) { // Uint8List? bytes = result.files.first // .bytes; // Access the bytes property // String? name = result.files.first.name; // // final blob = html.Blob([bytes]); // // final blobUrl = // // html.Url.createObjectUrlFromBlob(blob); // state.file = FilesModel( // "", // No need for a file path on web // name: name, // bytes: bytes, // audio: true, // image: false, // ); // } else { // state.file = FilesModel(result.files.single.path!, // audio: true, image: false); // } // } // await Future.delayed( // Duration.zero, // () => ActionSheetUtils(context).pop(), // ); // }, // ) // ], // ), // )); // } AnimatedVisibility attachmentLayout(AiChatState state, BuildContext context) { return AnimatedVisibility( isVisible: openAttach, fadeMode: FadeMode.horizontal, duration: DesignConfig.lowAnimationDuration, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (historyState.bot!.attachmentType!.contains('pdf')) MessageBarBtn( enable: true, icon: CupertinoIcons.doc_fill, click: () async { MediaService.onLoadingPickFile(context); FilePickerResult? result = await MediaService.pickPdfFile(); if (result != null) { String? name = result.files.first.name; if (kIsWeb) { Uint8List? bytes = result.files.first.bytes; // Access the bytes property // Store bytes and file name directly in your state or model state.file = FilesModel( '', // No need for a file path on web name: name, bytes: bytes, audio: false, image: false, ); } else { state.file = FilesModel(result.files.single.path!, audio: false, image: false, name: name); } } Future.delayed( Duration.zero, () => ActionSheetUtils(context).pop(), ); openAttach = !openAttach; state.update(); }, ), if (historyState.bot!.attachmentType!.contains('image')) Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: MessageBarBtn( enable: true, icon: CupertinoIcons.photo, click: () async { MediaService.onLoadingPickFile(context); 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) { await Future.delayed( Duration.zero, () => ActionSheetUtils(context).pop(), ); return; } } if (pickedFile == null) { await Future.delayed( Duration.zero, () => ActionSheetUtils(context).pop(), ); return; } state.file = kIsWeb ? FilesModel(pickedFile.path, name: pickedFile.name, image: true, audio: false) : FilesModel(file!.path, name: pickedFile.name, image: true, audio: false); await Future.delayed( Duration.zero, () => ActionSheetUtils(context).pop(), ); openAttach = !openAttach; state.update(); }, ), ), // if (!kIsWeb && !Platform.isIOS) if (historyState.bot!.attachmentType!.contains('audio')) MessageBarBtn( enable: true, icon: CupertinoIcons.music_note_2, click: () async { MediaService.onLoadingPickFile(context); FilePickerResult? result = await MediaService.pickAudioFile(); if (result != null) { String? name = result.files.first.name; if (kIsWeb) { Uint8List? bytes = result.files.first.bytes; // Access the bytes property final blob = html.Blob([bytes]); final blobUrl = html.Url.createObjectUrlFromBlob(blob); state.file = FilesModel( blobUrl, // No need for a file path on web name: name, bytes: bytes, audio: true, image: false, ); } else { state.file = FilesModel(result.files.single.path!, name: name, audio: true, image: false); } } await Future.delayed( Duration.zero, () => ActionSheetUtils(context).pop(), ); openAttach = !openAttach; state.update(); }, ) ], )); } // InkWell attachBtn( // {required final String title, // required final IconData icon, // final Color? color, // final Function()? click}) { // final state = context.read(); // return InkWell( // onTap: () async { // await click?.call(); // state.update(); // }, // child: Column( // children: [ // Container( // width: 40, // height: 40, // decoration: BoxDecoration(shape: BoxShape.circle, color: color), // padding: const EdgeInsets.all(0.8), // margin: const EdgeInsets.symmetric(horizontal: 12), // child: Icon( // icon, // size: 20, // color: Theme.of(context).colorScheme.white, // )), // const SizedBox( // height: 4, // ), // DidvanText( // title, // fontSize: 12, // ) // ], // ), // ); // } Widget audioContainer() { final state = context.watch(); return SizedBox( width: MediaQuery.sizeOf(context).width, child: AudioWave( file: state.file!.path, loadingPaddingSize: 8.0, totalDuration: _countTimer.value, ), ); } Widget fileContainer() { final state = context.watch(); return Container( decoration: BoxDecoration( borderRadius: DesignConfig.mediumBorderRadius, color: Theme.of(context).colorScheme.border, ), margin: const EdgeInsets.fromLTRB(4, 4, 4, 8), padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), child: Row( children: [ state.file != null && state.file!.isImage() ? SizedBox( width: 32, height: 42, child: ClipRRect( borderRadius: DesignConfig.lowBorderRadius, child: state.file!.isNetwork() ? Image.network( state.file!.path, fit: BoxFit.cover, ) : Image.file( state.file!.main, fit: BoxFit.cover, ))) : const Icon(Icons.file_copy), const SizedBox( width: 12, ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 24, child: MarqueeText( text: state.file != null ? state.file!.name! : '', 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, ); }) ], ), ) ], ), ); } }