didvan-app/lib/views/ai/create_bot_assistants_page....

948 lines
50 KiB
Dart

// ignore_for_file: deprecated_member_use
import 'dart:async';
import 'dart:io';
import 'package:animated_custom_dropdown/custom_dropdown.dart';
import 'package:cached_network_image/cached_network_image.dart';
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/bot_assistants_req_model.dart';
import 'package:didvan/models/ai/bots_model.dart';
import 'package:didvan/models/ai/file_create_assistants_model.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/utils/extension.dart';
import 'package:didvan/views/ai/bot_assistants_state.dart';
import 'package:didvan/views/ai/create_bot_assistants_state.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/switch.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/didvan/text_field.dart';
import 'package:didvan/views/widgets/hoshan_app_bar.dart';
import 'package:didvan/views/widgets/marquee_text.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/skeleton_image.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:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:provider/provider.dart';
class CreateBotAssistantsPage extends StatefulWidget {
final int? id;
const CreateBotAssistantsPage({Key? key, this.id}) : super(key: key);
@override
State<CreateBotAssistantsPage> createState() =>
_CreateBotAssistantsPageState();
}
class _CreateBotAssistantsPageState extends State<CreateBotAssistantsPage> {
late InputBorder defaultBorder = OutlineInputBorder(
borderRadius: DesignConfig.mediumBorderRadius,
borderSide: BorderSide(
width: 1, color: Theme.of(context).colorScheme.disabledText));
// final _formYouTubeKey = GlobalKey<FormState>();
final _formNameKey = GlobalKey<FormState>();
final _formPromptKey = GlobalKey<FormState>();
final _formDescKey = GlobalKey<FormState>();
Timer? _timer;
@override
void initState() {
super.initState();
}
void onConfirm(CreateBotAssistantsState state) async {
bool isValid = true;
// if (!_formYouTubeKey.currentState!.validate()) {
// isValid = false;
// }
if (!_formNameKey.currentState!.validate()) {
isValid = false;
}
if (!_formDescKey.currentState!.validate()) {
isValid = false;
}
if (!_formPromptKey.currentState!.validate()) {
isValid = false;
}
if (!isValid) {
return;
}
final List<XFile> resultFiles = [];
for (var file in state.files) {
if (!file.fromNetwork) {
resultFiles.add(file.file!);
}
}
final success = await state.createAssistants(
id: widget.id,
data: BotAssistantsReqModel(
type: state.selectedBotType,
name: state.name,
description: state.desc,
botId: state.initialBot!.id!,
prompt: state.prompt,
webLinks:
state.countOfLink.first.isEmpty ? null : state.countOfLink,
// youtubeLink: youtubeLink,
isPrivate: state.isPrivate,
files: resultFiles,
deleteImage: state.assistant != null
? state.assistant!.image == null
: state.image.value == null,
image: state.image.value));
if (success) {
context.read<BotAssistantsState>().getMyAssissmant();
context.read<CreateBotAssistantsState>().assistant = null;
Navigator.pop(context);
} else {
ActionSheetUtils(context).showAlert(AlertData(
message: 'مشکلی در ارتباط با سرور پیش آمده دوباره تلاش کنید',
aLertType: ALertType.error));
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
context.read<CreateBotAssistantsState>().assistant = null;
return true;
},
child: Scaffold(
appBar: HoshanAppBar(
onBack: () {
context.read<CreateBotAssistantsState>().assistant = null;
Navigator.pop(context);
},
withActions: false,
),
body: Consumer<CreateBotAssistantsState>(builder: (BuildContext context,
CreateBotAssistantsState state, Widget? child) {
return state.loading
? Center(
child: SpinKitThreeBounce(
size: 46,
color: Theme.of(context).colorScheme.primary,
),
)
: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0, vertical: 32),
child: Column(
children: [
title(text: 'نوع دستیار'),
SizedBox(
width: MediaQuery.sizeOf(context).width,
child: CustomDropdown<String>(
closedHeaderPadding: const EdgeInsets.all(12),
items: state.botModels,
enabled: state.assistant == null,
initialItem: state.selectedBotType == 'text'
? state.botModels.first
: state.botModels.last,
hideSelectedFieldWhenExpanded: false,
decoration: CustomDropdownDecoration(
listItemDecoration: ListItemDecoration(
selectedColor: Theme.of(context)
.colorScheme
.surface
.withOpacity(0.5),
),
closedBorder: Border.all(color: Colors.grey),
closedFillColor:
Theme.of(context).colorScheme.surface,
expandedFillColor:
Theme.of(context).colorScheme.surface),
// hintText: "انتخاب کنید",
onChanged: (value) {
final index = state.botModels.indexOf(value!);
state.selectedBotType =
index == 0 ? 'text' : 'image';
state.update();
},
),
),
const SizedBox(
height: 24,
),
ValueListenableBuilder(
valueListenable: state.image,
builder: (context, img, _) {
return Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
DidvanButton(
width: 120,
style: ButtonStyleMode.flat,
color: state.image.value != null ||
state.assistant?.image != null
? Theme.of(context).colorScheme.error
: null,
title: state.image.value != null ||
state.assistant?.image != null
? 'حذف عکس'
: 'انتخاب عکس',
onPressed: () async {
if (state.image.value != null ||
state.assistant?.image != null) {
if (state.assistant != null) {
state.assistant!.image = null;
}
state.image.value = null;
return;
}
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) {
state.image.value = XFile(file.path);
}
}
},
),
img != null
? ClipRRect(
borderRadius:
DesignConfig.lowBorderRadius,
child: SizedBox(
width: 80,
height: 80,
child:
Image.file(File(img.path))),
)
: SkeletonImage(
imageUrl: state.assistant != null &&
state.assistant!.image != null
? state.assistant!.image!
: 'https://via.placeholder.com/70x70',
width: 80,
height: 80,
)
],
);
}),
const SizedBox(
height: 24,
),
title(text: 'انتخاب نام'),
Form(
key: _formNameKey,
child: DidvanTextField(
initialValue: state.name,
onChanged: (value) {
state.name = value;
if (value.isEmpty) {
return;
}
if (state.assistant == null) {
_timer?.cancel();
_timer =
Timer(const Duration(seconds: 1), () async {
await state.getAssistantsName(name: value);
_formNameKey.currentState!.validate();
});
}
},
validator: (value) {
String? result;
if (value.isEmpty) {
result = 'نام نباید خالی باشد';
} else if (value.length < 4) {
result = 'نام نباید کمتر از 4 حرف باشد';
} else if (state.assistant == null &&
!state.successName) {
result =
'اسم دیگری انتخاب کنید این اسم موجود است';
}
return result;
},
hintText: 'ai@2024_B',
maxLength: 20,
),
),
const SizedBox(
height: 8,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
state.loadingName
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator())
: Icon(
DidvanIcons.info_circle_light,
color:
Theme.of(context).colorScheme.caption,
),
const SizedBox(width: 4),
Expanded(
child: DidvanText(
state.loadingName
? '...درحال بررسی اسم'
: 'نام منحصر به فرد شامل 4 تا 20 کاراکتر (حروف، اعداد، خط تیره، نقطه و زیرخط) ',
textAlign: TextAlign.right,
fontSize: 12,
color: Theme.of(context).colorScheme.caption,
),
),
],
),
const SizedBox(
height: 24,
),
title(text: 'توضیحات بات'),
Form(
key: _formDescKey,
child: DidvanTextField(
initialValue: state.desc,
hintText:
'توضیح دهید چه کارهایی از این ربات بر می‌آید.',
textInputType: TextInputType.multiline,
minLine: 4,
maxLine: 4,
maxLength: 200,
hasHeight: false,
showLen: true,
onChanged: (value) {
state.desc = value;
},
validator: (value) {
String? result;
if (value.isEmpty) {
result = 'توضیحات نباید خالی باشد';
} else if (value.length < 10) {
result = 'توضیحات نباید کمتر از 10 حرف باشد';
}
return result;
},
),
),
const SizedBox(
height: 24,
),
title(text: 'نوع دستیار'),
state.loadingImageBots
? ShimmerPlaceholder(
width: MediaQuery.sizeOf(context).width,
height: 48,
borderRadius: DesignConfig.lowBorderRadius,
)
: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: CustomDropdown<BotsModel>(
closedHeaderPadding: const EdgeInsets.all(12),
items: state.selectedBotType == 'text'
? state.allBots
: state.imageBots,
headerBuilder: (context, bot, enabled) =>
botRow(bot, context),
listItemBuilder: (context, bot, isSelected,
onItemSelect) =>
botRow(bot, context),
initialItem: state.assistant != null
? state.initialBot
: (state.selectedBotType == 'text'
? state.allBots
: state.imageBots)
.first,
hideSelectedFieldWhenExpanded: false,
decoration: CustomDropdownDecoration(
listItemDecoration: ListItemDecoration(
selectedColor: Theme.of(context)
.colorScheme
.surface
.withOpacity(0.5),
),
closedFillColor:
Theme.of(context).colorScheme.surface,
closedBorder:
Border.all(color: Colors.grey),
expandedFillColor: Theme.of(context)
.colorScheme
.surface),
// hintText: "انتخاب کنید",
onChanged: (value) {
state.initialBot = value;
},
),
),
const SizedBox(
height: 24,
),
title(text: 'دستورالعمل'),
Form(
key: _formPromptKey,
child: DidvanTextField(
initialValue: state.prompt,
hintText:
'به ربات خود بگویید که چگونه رفتار کند و چگونه به پیام‌های کاربر پاسخ دهد. سعی کنید تا حد امکان پیام واضح و مشخص باشد.',
textInputType: TextInputType.multiline,
minLine: 6,
maxLine: 6,
maxLength: 400,
hasHeight: false,
showLen: true,
onChanged: (value) {
state.prompt = value;
},
validator: (value) {
String? result;
if (value.isEmpty) {
result = 'دستورالعمل نباید خالی باشد';
} else if (value.length < 10) {
result = 'نام نباید کمتر از 10 حرف باشد';
}
return result;
},
),
),
const SizedBox(
height: 24,
),
if (state.assistant == null)
Column(
children: [
if (state.selectedBotType == 'text')
Column(
children: [
title(
text: 'پایگاه دانش', isRequired: false),
if (state.files.length != 3)
SizedBox(
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context)
.colorScheme
.disabledBackground,
shape:
const RoundedRectangleBorder(
borderRadius: DesignConfig
.lowBorderRadius)),
onPressed: () async {
final picks = await MediaService
.pickMultiFile();
if (picks != null) {
for (var file in picks.xFiles) {
if (file.path.isDocument() ||
file.path.isAudio()) {
state.files.add(
FileCreateAssistantsModel(
fromNetwork: false,
file: file,
url: null));
} else {
ActionSheetUtils(context)
.showAlert(AlertData(
message:
'باید فایل انتخاب شده صوتی یا Pdf باشد',
aLertType: ALertType
.error));
}
}
}
state.update();
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
CupertinoIcons.add,
color: Theme.of(context)
.colorScheme
.caption,
),
const SizedBox(
width: 4,
),
DidvanText(
'آپلود فایل (فایل صوتی، پی دی اف)',
color: Theme.of(context)
.colorScheme
.caption,
fontSize: 16,
)
],
)),
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
...List.generate(
state.files.length,
(index) {
return SizedBox(
child: Stack(
children: [
Container(
width: MediaQuery
.sizeOf(
context)
.width /
5,
height: MediaQuery.sizeOf(
context)
.width /
5,
margin: const EdgeInsets
.symmetric(
horizontal: 8,
vertical: 12),
padding:
const EdgeInsets.all(
8),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.disabledBackground,
borderRadius: DesignConfig
.lowBorderRadius),
child: Column(
children: [
Expanded(
child: state
.files[
index]
.fromNetwork
? state
.files[
index]
.url!
.isImage()
? CachedNetworkImage(
imageUrl: state
.files[
index]
.url!)
: const Icon(
CupertinoIcons
.doc)
: state
.files[
index]
.file!
.path
.isImage()
? Image.file(File(state
.files[
index]
.file!
.path))
: const Icon(
CupertinoIcons
.doc),
),
MarqueeText(
text: state
.files[
index]
.fromNetwork
? state
.files[
index]
.url!
.split(
'/')
.last
: state
.files[
index]
.file!
.name,
textDirection:
TextDirection
.rtl,
style: Theme.of(
context)
.textTheme
.labelSmall!)
],
)),
Positioned(
top: 8,
left: 4,
child: InkWell(
onTap: () {
state.files
.removeAt(index);
state.update();
},
child: Container(
padding:
const EdgeInsets
.all(6),
decoration: BoxDecoration(
shape: BoxShape
.circle,
color: Theme.of(
context)
.colorScheme
.error),
child: const Icon(
DidvanIcons
.trash_solid,
color: Colors.white,
size: 18,
),
),
))
],
),
);
},
)
],
),
const SizedBox(
height: 24,
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
title(
text: 'لینک وب سایت',
isRequired: false),
Row(
children: [
if (state.countOfLink.length > 1)
DidvanIconButton(
icon: CupertinoIcons
.minus_circle_fill,
onPressed: () {
state.countOfLink
.removeLast();
state.update();
},
),
if (state.countOfLink.length != 3)
DidvanIconButton(
icon: CupertinoIcons
.plus_circle_fill,
onPressed: () {
if (state.countOfLink.last
.isNotEmpty) {
state.countOfLink.add('');
state.update();
}
},
),
],
)
],
),
ListView.builder(
shrinkWrap: true,
itemCount: state.countOfLink.length,
physics:
const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Column(
children: [
DidvanTextField(
onChanged: (value) {
state.countOfLink[index] = value;
if (state.countOfLink[index]
.isEmpty &&
state.countOfLink.length !=
1) {
state.countOfLink
.removeAt(index);
}
state.update();
},
// validator: (value) {},
hintText:
'https://www.weforum.org/agenda/2024/08',
),
const SizedBox(
height: 8,
),
],
),
),
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Icon(
DidvanIcons.info_circle_light,
color: Theme.of(context)
.colorScheme
.caption,
),
const SizedBox(width: 4),
Expanded(
child: DidvanText(
'دستیار شما با استناد بر اطلاعات ارائه شده در پایگاه دانش، پیام کاربران را ارزیابی می‌کند.',
textAlign: TextAlign.right,
fontSize: 12,
color: Theme.of(context)
.colorScheme
.caption,
),
),
],
),
const SizedBox(
height: 24,
),
],
),
// title(text: 'لینک یوتیوب', isRequired: false),
// Form(
// key: _formYouTubeKey,
// child: DidvanTextField(
// onChanged: (value) {
// youtubeLink = value;
// },
// validator: (value) =>
// value.startsWith('https://www.youtube.com') ||
// value.isEmpty
// ? null
// : 'باید لینک یوتیوب باشد',
// hintText: 'https://www.youtube.com/watch?v',
// ),
// ),
// const SizedBox(
// height: 24,
// ),
],
),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const DidvanText(
'نمایش عمومی',
fontSize: 14,
),
Row(
children: [
Expanded(
child: DidvanText(
'در صورت فعال بودن، دستیار شما توسط سایرین قابل مشاهده بوده و مورد استفاده قرار می‌گیرد.',
fontSize: 14,
color: Theme.of(context)
.colorScheme
.caption,
),
),
],
)
],
),
),
SizedBox(
width: 64,
height: 48,
child: DidvanSwitch(
value: !state.isPrivate,
title: '',
onChanged: (value) {
state.isPrivate = !value;
},
),
),
],
),
const SizedBox(
height: 24,
),
widget.id != null
? Flex(
direction: Axis.horizontal,
children: [
Flexible(
flex: 2,
child: Stack(
children: [
DidvanButton(
title: state.loadingCreate
? ' '
: 'ذخیره تغییرات',
onPressed: () async =>
onConfirm(state),
),
if (state.loadingCreate)
const Positioned.fill(
child: Center(
child: SpinKitThreeBounce(
size: 32,
color: Colors.white,
),
),
)
],
),
),
const SizedBox(
width: 20,
),
Flexible(
flex: 1,
child: DidvanButton(
title: 'حذف دستیار',
style: ButtonStyleMode.flat,
color:
Theme.of(context).colorScheme.error,
onPressed: () {
ActionSheetUtils(context).openDialog(
data: ActionSheetData(
title: 'حذف دستیار',
titleIcon: DidvanIcons.trash_solid,
titleColor: Theme.of(context)
.colorScheme
.error,
content: const Column(
children: [
DidvanText(
'با حذف این دستیار، استفاده از آن برای شما و سایر کاربران، امکان‌پذیر نیست.\nآیا مطمئن هستید؟!',
fontSize: 14,
)
],
),
onConfirmed: () async {
final success =
await state.deleteAssistants(
id: widget.id!);
if (success) {
context
.read<BotAssistantsState>()
.getMyAssissmant();
context
.read<
CreateBotAssistantsState>()
.assistant = null;
Navigator.pop(context);
}
},
));
},
),
),
],
)
: Stack(
children: [
DidvanButton(
title:
state.loadingCreate ? ' ' : 'ذخیره',
onPressed: () async => onConfirm(state)),
if (state.loadingCreate)
const Positioned.fill(
child: Center(
child: SpinKitThreeBounce(
size: 32,
color: Colors.white,
),
),
)
],
),
const SizedBox(
height: 24,
),
],
),
),
);
}),
),
);
}
Container botRow(BotsModel bot, BuildContext context) {
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: DesignConfig.highBorderRadius),
child: Row(
children: [
SkeletonImage(
imageUrl: bot.image.toString(),
width: 42,
height: 42,
borderRadius: BorderRadius.circular(360),
),
const SizedBox(width: 12),
Expanded(
child: DidvanText(
bot.name.toString(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
))
],
),
);
}
Widget title({required final String text, final bool isRequired = true}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Text.rich(
TextSpan(
children: [
TextSpan(
text: text,
style: Theme.of(context).textTheme.bodyMedium,
),
if (isRequired)
TextSpan(
text: '*',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.error)),
],
),
),
],
),
);
}
}