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

634 lines
31 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/storage/storage.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> {
TextEditingController message = TextEditingController();
@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.white,
borderRadius: BorderRadius.circular(360)),
child: Row(
children: [
const SizedBox(
width: 8,
),
Expanded(
child: StreamBuilder<RecordState>(
stream: record.onStateChanged(),
builder: (context, snapshot) {
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') &&
message.text.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: message
.text.isNotEmpty ||
state.file != null,
icon:
DidvanIcons.send_light,
click: () async {
if (state.file == null &&
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: 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: 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(),
)
]));
}
message.clear();
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: 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),
),
onChanged: (value) {
setState(() {});
},
)),
),
),
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,
),
if (context
.read<HistoryAiChatState>()
.bot!
.attachmentType!
.isNotEmpty &&
(widget.bot.attachment != 0 &&
(widget.bot.attachment == 1 &&
message.text.isEmpty)) ||
widget.bot.attachment == 2)
SizedBox(
width: 46,
height: 46,
child: Center(
child: PopupMenuButton(
onOpened: () {
},
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(
0, widget.focusNode!.hasFocus ? -999 : 999),
position: PopupMenuPosition.over,
useRootNavigator: true,
child: Icon(
Icons.attach_file_rounded,
color:
Theme.of(context).colorScheme.focusedBorder,
),
),
)),
],
),
],
),
);
},
);
}
AnimatedVisibility audioContainer() {
final state = context.watch<AiChatState>();
return AnimatedVisibility(
isVisible: state.file != null &&
lookupMimeType(state.file!.path)!.startsWith('audio/'),
duration: DesignConfig.lowAnimationDuration,
child: Directionality(
textDirection: TextDirection.ltr,
child: FutureBuilder(
future: StorageService.getValue(key: 'token'),
builder: (context, snapshot) => 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.white,
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')
? 'https://api.didvan.app${state.file!.path}?accessToken=${snapshot.data}'
: 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,
),
),
),
),
),
));
}
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))
],
),
),
);
}
}