didvan-app/lib/views/ai/widgets/ai_message_bar_ios.dart

817 lines
45 KiB
Dart

// 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/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/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<AiMessageBarIOS> createState() => _AiMessageBarIOSState();
}
class _AiMessageBarIOSState extends State<AiMessageBarIOS> {
final ValueNotifier<String> messageText = ValueNotifier('');
bool openAttach = false;
String? path;
late HistoryAiChatState historyState = context.read<HistoryAiChatState>();
@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<Duration> _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<AiChatState>(
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<RecordState>(
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
.replaceAll(
r"\n",
r" \n \n "),
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
.replaceAll(
r"\n",
r" \n \n "),
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: 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
// 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<AiChatState>();
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);
CroppedFile? file;
if (pickedFile != null && !kIsWeb) {
file = await ImageCropper().cropImage(
sourcePath: pickedFile.path,
uiSettings: [
AndroidUiSettings(toolbarTitle: 'برش تصویر'),
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<AiChatState>();
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,
);
})
],
),
)
],
),
);
}
}