// ignore_for_file: deprecated_member_use 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/media/voice.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_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: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; class AiMessageBarIOS extends StatefulWidget { final BotsModel bot; final String? assistantsName; const AiMessageBarIOS({ super.key, required this.bot, this.assistantsName, }); @override State createState() => _AiMessageBarIOSState(); } class _AiMessageBarIOSState extends State { final ValueNotifier messageText = ValueNotifier(''); bool openAttach = false; String? path; late HistoryAiChatState historyState = context.read(); @override void initState() { 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(Duration.zero); void startTimer() { const oneSec = Duration(seconds: 1); _timer = Timer.periodic( oneSec, (Timer timer) { _countTimer.value = Duration(seconds: _countTimer.value.inSeconds + 1); }, ); } @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( // height: 50, 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( children: [ const SizedBox( width: 8, ), Expanded( child: StreamBuilder( stream: record.onStateChanged(), builder: (context, snapshot) { return ValueListenableBuilder( valueListenable: messageText, builder: (context, value, child) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Padding( padding: const EdgeInsets.only( bottom: 8.0), child: ( // !kIsWeb && snapshot.hasData && snapshot.data! != RecordState.stop) ? MessageBarBtn( enable: true, icon: DidvanIcons .stop_circle_solid, click: () async { path = await record.stop(); Duration? duration = await VoiceService .getDuration( src: path ?? ''); state.file = FilesModel( path.toString(), name: '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a', isRecorded: true, audio: true, image: false, duration: duration); _timer.cancel(); state.update(); }, ) : // (!kIsWeb && // !Platform // .isIOS) && widget.bot.attachmentType! .contains( 'audio') && value.isEmpty && state.file == null && widget.bot.attachment != 0 ? MessageBarBtn( enable: true, icon: DidvanIcons .mic_regular, click: () async { if (await record .hasPermission()) { try { String path = '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a'; // if (!kIsWeb) { Directory? downloadDir = await getApplicationDocumentsDirectory(); path = p.join( downloadDir .path, '${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a'); } setState( () { openAttach = false; }, ); record.start( const RecordConfig(), path: path); startTimer(); } catch (e) { if (kDebugMode) { print( 'Error starting recording: $e'); } } } }, ) : MessageBarBtn( enable: (state .file != null && state.file! .isRecorded) || (widget.bot .attachment == 1) || value.isNotEmpty, icon: DidvanIcons .send_light, click: () async { if ((state.file == null || !state.file! .isRecorded) && (widget.bot .attachment != 1) && 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?.name, 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 ?.name, fileLocal: state .file, role: 'user', createdAt: DateTime .now() .subtract(const Duration( minutes: 210)) .toIso8601String(), ) ])); } state.message.clear(); messageText.value = state .message.text; await state.postMessage( widget.bot, widget.assistantsName != null); }, ), ), 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 .spaceBetween, children: [ Directionality( textDirection: TextDirection .ltr, child: Row( mainAxisAlignment: MainAxisAlignment .center, children: List.generate( 4, (index) => snapshot.data! == RecordState.pause ? Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate( 8, (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)), ), )), ) : SpinKitWave( color: Theme.of(context).colorScheme.primary.withOpacity(0.4), size: 32, itemCount: 10, ))), ), ValueListenableBuilder< Duration>( valueListenable: _countTimer, builder: (context, value, child) => DidvanText(DateTimeUtils .normalizeTimeDuration( value)), ), ], ), ) : state.file != null && state.file!.isRecorded ? audioContainer() : Form( child: TextFormField( textInputAction: TextInputAction .newline, style: Theme.of(context) .textTheme .bodyMedium, minLines: 1, maxLines: 6, // Set this // expands: true, // // keyboardType: TextInputType.text, keyboardType: TextInputType .multiline, controller: state.message, 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; state.update(); }, )), ), ), if (snapshot.hasData) Padding( padding: const EdgeInsets.only( bottom: 8.0), child: 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, ), Padding( padding: const EdgeInsets.only( bottom: 8.0), child: attachmentLayout( state, context), ), if (!snapshot.hasData || (snapshot.hasData && snapshot.data == RecordState.stop)) Padding( padding: const EdgeInsets.fromLTRB( 12, 0, 12, 8), child: state.file != null || openAttach ? MessageBarBtn( click: () { if (openAttach) { setState(() { openAttach = !openAttach; }); return; } state.file = null; _countTimer.value = Duration.zero; state.update(); }, enable: false, icon: DidvanIcons .close_solid) : widget.bot.attachmentType! .isNotEmpty ? MessageBarBtn( click: () { setState(() { openAttach = !openAttach; }); }, enable: false, icon: Icons .attach_file_rounded, ) : const SizedBox(), ), ], ); }); }), ), ], ), ), if (state.onResponsing) Positioned.fill( child: Container( decoration: BoxDecoration( color: Theme.of(context) .colorScheme .focused .withOpacity(0.5)), child: Center( child: SpinKitThreeBounce( color: Theme.of(context).colorScheme.primary, size: 32, ), ), ), ) ], ), MediaQuery.of(context).viewInsets.bottom == 0 ? const Padding( padding: EdgeInsets.fromLTRB(8, 8, 8, 4), child: DidvanText( 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید.', fontSize: 12, ), ) : const SizedBox( height: 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, ), ); } 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) { Uint8List? bytes = result.files.first.bytes; // Access the bytes property String? name = result.files.first.name; if (kIsWeb) { // 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!, name: name, audio: false, image: false); } } 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, 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) { if (kIsWeb) { Uint8List? bytes = result.files.first.bytes; // Access the bytes property String? name = result.files.first.name; 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(), ); openAttach = !openAttach; state.update(); }, ) ], )); } 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, ); }) ], ), ) ], ), ); } }