792 lines
43 KiB
Dart
792 lines
43 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/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;
|
|
const AiMessageBarIOS({
|
|
super.key,
|
|
required this.bot,
|
|
});
|
|
|
|
@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,
|
|
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);
|
|
},
|
|
),
|
|
),
|
|
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<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);
|
|
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<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,
|
|
);
|
|
})
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|