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

683 lines
36 KiB
Dart

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<AiMessageBar> createState() => _AiMessageBarState();
static PopupMenuItem<dynamic> 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<AiMessageBar> {
final ValueNotifier<String> 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<int> _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<AiChatState>(
builder: (context, state, child) {
final historyState = context.read<HistoryAiChatState>();
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<RecordState>(
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<HistoryAiChatState>()
.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<AiChatState>();
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<AiChatState>();
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))
],
),
),
);
}
}