diff --git a/lib/main.dart b/lib/main.dart index 6fc951f..6173056 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'package:didvan/services/notification/firebase_api.dart'; import 'package:didvan/services/notification/notification_service.dart'; import 'package:didvan/utils/my_custom_scroll_behavior.dart'; import 'package:didvan/views/ai/ai_state.dart'; +import 'package:didvan/views/ai/bot_assistants_state.dart'; import 'package:didvan/views/ai/create_bot_assistants_state.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/ai/tools_state.dart'; @@ -182,6 +183,9 @@ class _DidvanState extends State with WidgetsBindingObserver { ChangeNotifierProvider( create: (context) => CreateBotAssistantsState(), ), + ChangeNotifierProvider( + create: (context) => BotAssistantsState(), + ), ], child: Consumer( builder: (context, themeProvider, child) => Container( diff --git a/lib/models/ai/bot_assistants_model.dart b/lib/models/ai/bot_assistants_model.dart index a9864d7..3677a93 100644 --- a/lib/models/ai/bot_assistants_model.dart +++ b/lib/models/ai/bot_assistants_model.dart @@ -25,12 +25,19 @@ class BotAssistantsModel { class BotAssistants { int? id; + int? userId; + int? botId; String? name; String? description; String? createdAt; String? image; + String? type; + String? prompt; + bool? private; BotsModel? bot; User? user; + List? files; + List? websites; BotAssistants( {this.id, @@ -39,16 +46,40 @@ class BotAssistants { this.createdAt, this.image, this.bot, - this.user}); + this.private, + this.user, + this.botId, + this.prompt, + this.type, + this.websites, + this.files, + this.userId}); BotAssistants.fromJson(Map json) { id = json['id']; + userId = json['userId']; + botId = json['botId']; name = json['name']; description = json['description']; createdAt = json['createdAt']; image = json['image']; + type = json['type']; + prompt = json['prompt']; + private = json['private']; bot = json['bot'] != null ? BotsModel.fromJson(json['bot']) : null; user = json['user'] != null ? User.fromJson(json['user']) : null; + if (json['files'] != null) { + files = []; + json['files'].forEach((v) { + files!.add(v); + }); + } + if (json['websites'] != null) { + websites = []; + json['websites'].forEach((v) { + websites!.add(v); + }); + } } Map toJson() { @@ -58,6 +89,7 @@ class BotAssistants { data['description'] = description; data['createdAt'] = createdAt; data['image'] = image; + data['private'] = private; if (bot != null) { data['bot'] = bot!.toJson(); } diff --git a/lib/models/ai/bot_assistants_req_model.dart b/lib/models/ai/bot_assistants_req_model.dart index fe5bd3e..cc3bffa 100644 --- a/lib/models/ai/bot_assistants_req_model.dart +++ b/lib/models/ai/bot_assistants_req_model.dart @@ -21,4 +21,20 @@ class BotAssistantsReqModel { this.youtubeLink, this.webLinks, this.isPrivate = true}); + + Map toJson() { + final Map data = {}; + data['type'] = type; + data['name'] = name; + data['botId'] = botId; + data['prompt'] = prompt; + data['isPrivate'] = isPrivate; + if (youtubeLink != null) { + data['youtubeLink'] = youtubeLink; + } + if (webLinks != null) { + data['webLinks'] = webLinks; + } + return data; + } } diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index f38c2ba..9108744 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -322,7 +322,9 @@ class RouteGenerator { case Routes.botAssistants: return _createRoute(const BotAssistantsPage()); case Routes.createBotAssistants: - return _createRoute(const CreateBotAssistantsPage()); + return _createRoute(CreateBotAssistantsPage( + id: settings.arguments as int?, + )); case Routes.info: return _createRoute(const InfoPage()); diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 777fc41..6607b87 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -137,6 +137,44 @@ class MediaService { } } + static Future pickMultiFile() async { + try { + return await FilePicker.platform + .pickFiles( + type: FileType.custom, + allowedExtensions: [ + 'pdf', + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'txt', + 'mp3', + 'wav', + 'aac', + 'ogg', + 'flac' + ], // You can specify allowed extensions if needed + allowMultiple: true, + // Note: The maxFiles parameter is not directly supported by FilePicker. + // You will need to handle the limit after selection if necessary. + ) + .then((result) { + if (result != null && result.files.length > 3) { + // Handle the case where the selected files exceed the max limit + // You can show an error message or return null + return null; // or show a message to the user + } + return result; + }); + } catch (e) { + e.printError(info: 'Pick Multi File Fail'); + return null; + } + } + static Future pickAudioFile() async { try { return await FilePicker.platform.pickFiles( diff --git a/lib/services/network/request.dart b/lib/services/network/request.dart index 13528c9..0234140 100644 --- a/lib/services/network/request.dart +++ b/lib/services/network/request.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; import 'package:didvan/services/storage/storage.dart'; // ignore: depend_on_referenced_packages @@ -10,6 +10,8 @@ import 'package:http/http.dart' as http; // ignore: depend_on_referenced_packages import 'package:http_parser/http_parser.dart' as parser; +import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; import 'package:permission_handler/permission_handler.dart'; class RequestService { @@ -119,6 +121,71 @@ class RequestService { } } + Future multipartFilesCreateAssismants({ + required List? files, + required XFile? image, + required String method, + }) async { + try { + final request = http.MultipartRequest(method, Uri.parse(url)); + _headers.update('Content-Type', (_) => 'multipart/form-data'); + request.headers.addAll(_headers); + if (_requestBody != null) { + _requestBody!.forEach((key, value) { + request.fields.addAll({key.toString(): value.toString()}); + }); + } + if (files != null) { + for (var file in files) { + final length = await file.length(); + final mimeType = lookupMimeType(file.path) ?? + 'application/octet-stream'; // Get content type + + request.files.add( + http.MultipartFile( + 'files', + file.readAsBytes().asStream(), + length, + filename: file.name, + contentType: + parser.MediaType.parse(mimeType), // Use the content type + ), + ); + } + } + + if (image != null) { + final length = await image.length(); + final mimeType = lookupMimeType(image.path) ?? + 'application/octet-stream'; // Get content type + + request.files.add( + http.MultipartFile( + 'image', + image.readAsBytes().asStream(), + length, + filename: image.name, + contentType: + parser.MediaType.parse(mimeType), // Use the content type + ), + ); + } + + final streamedResponse = await request + .send() + .timeout( + const Duration(seconds: 30), + ) + .catchError( + (e) => throw e, + ); + final response = await http.Response.fromStream(streamedResponse); + _handleResponse(response); + } catch (e) { + _handleError(null); + } + } + Future multipart({ required dynamic file, required String method, @@ -233,7 +300,11 @@ class RequestService { void _handleResponse(http.Response? response) { statusCode = response?.statusCode; - + if (response != null) { + if (kDebugMode) { + print('Response from [$url]: ${response.body}'); + } + } if (_handleError(response)) { if (response!.body.isNotEmpty) { _body = json.decode(response.body); diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index 33011bd..02544da 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -236,6 +236,8 @@ class RequestHelper { ])}'; static String createAssistants() => '$baseUrl/ai/bot'; static String updateAssistants(int id) => '$baseUrl/ai/bot/$id'; + static String getAssistant(int id) => '$baseUrl/ai/bot/user/$id'; + static String deleteAssistant(int id) => '$baseUrl/ai/bot/user/$id'; static String _urlConcatGenerator(List> additions) { String result = ''; diff --git a/lib/utils/action_sheet.dart b/lib/utils/action_sheet.dart index 5fda755..f2361ff 100644 --- a/lib/utils/action_sheet.dart +++ b/lib/utils/action_sheet.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:ui'; import 'package:bot_toast/bot_toast.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/assets.dart'; @@ -377,12 +376,12 @@ class ActionSheetUtils { width: 1))), child: Row( children: [ - ClipOval( - child: CachedNetworkImage( - imageUrl: bot.image.toString(), - width: 42, - height: 42, - ), + SkeletonImage( + imageUrl: bot.image.toString(), + width: 42, + height: 42, + borderRadius: + BorderRadius.circular(360), ), const SizedBox(width: 12), Expanded( diff --git a/lib/views/ai/ai.dart b/lib/views/ai/ai.dart index c47556f..532899c 100644 --- a/lib/views/ai/ai.dart +++ b/lib/views/ai/ai.dart @@ -2,7 +2,6 @@ import 'dart:math'; -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'; @@ -17,6 +16,7 @@ import 'package:didvan/views/ai/tools_screen.dart'; import 'package:didvan/views/ai/widgets/message_bar_btn.dart'; import 'package:didvan/views/home/home.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -154,12 +154,12 @@ class _AiState extends State { const SizedBox( width: 12, ), - ClipOval( - child: CachedNetworkImage( - width: 46, - height: 46, - imageUrl: bot.image.toString(), - ), + SkeletonImage( + width: 46, + height: 46, + imageUrl: bot.image.toString(), + borderRadius: + BorderRadius.circular(360), ), ], ), diff --git a/lib/views/ai/ai_chat_page.dart b/lib/views/ai/ai_chat_page.dart index 064f249..440ac7f 100644 --- a/lib/views/ai/ai_chat_page.dart +++ b/lib/views/ai/ai_chat_page.dart @@ -2,7 +2,6 @@ import 'dart:io'; -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'; @@ -133,12 +132,11 @@ class _AiChatPageState extends State { const SizedBox( height: 24, ), - ClipOval( - child: CachedNetworkImage( - width: 75, - height: 75, - imageUrl: widget.args.bot.image.toString(), - ), + SkeletonImage( + width: 75, + height: 75, + imageUrl: widget.args.bot.image.toString(), + borderRadius: BorderRadius.circular(360), ), const SizedBox( height: 12, @@ -467,15 +465,15 @@ class _AiChatPageState extends State { maxWidth: 200), child: Row( children: [ - ClipOval( - child: - CachedNetworkImage( - imageUrl: bots[index] - .image - .toString(), - width: 42, - height: 42, - ), + SkeletonImage( + imageUrl: bots[index] + .image + .toString(), + width: 42, + height: 42, + borderRadius: + BorderRadius + .circular(360), ), const SizedBox(width: 12), Expanded( diff --git a/lib/views/ai/bot_assistants_page.dart b/lib/views/ai/bot_assistants_page.dart index 8fed8ab..a0efaa3 100644 --- a/lib/views/ai/bot_assistants_page.dart +++ b/lib/views/ai/bot_assistants_page.dart @@ -3,16 +3,20 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/ai/ai_chat_args.dart'; import 'package:didvan/models/ai/bot_assistants_model.dart'; +import 'package:didvan/models/enums.dart'; import 'package:didvan/routes/routes.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/text.dart'; import 'package:didvan/views/widgets/hoshan_app_bar.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:didvan/views/widgets/state_handlers/empty_list.dart'; +import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; +import 'package:provider/provider.dart'; class BotAssistantsPage extends StatefulWidget { const BotAssistantsPage({Key? key}) : super(key: key); @@ -22,8 +26,6 @@ class BotAssistantsPage extends StatefulWidget { } class _BotAssistantsPageState extends State { - bool isMyAssistants = true; - @override Widget build(BuildContext context) { return Scaffold( @@ -31,302 +33,306 @@ class _BotAssistantsPageState extends State { onBack: () => Navigator.pop(context), withActions: false, ), - floatingActionButtonLocation: FloatingActionButtonLocation.startFloat, - floatingActionButton: isMyAssistants - ? FloatingActionButton.extended( - label: const DidvanText( - 'ایجاد دستیار جدید', - color: Colors.white, - ), - icon: const Icon( - Icons.add, - color: Colors.white, - ), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + floatingActionButton: context.watch().isMyAssistants + ? FloatingActionButton.small( + shape: const CircleBorder(), backgroundColor: Theme.of(context).colorScheme.primary, onPressed: () { Navigator.pushNamed(context, Routes.createBotAssistants); }, + child: const Icon( + Icons.add, + color: Colors.white, + ), ) : null, - body: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Center( - child: Padding( - padding: EdgeInsets.only(top: 32, bottom: 24), - child: DidvanText( - 'انتخاب بات‌ها', - fontSize: 20, - fontWeight: FontWeight.bold, - color: Color(0xff1B3C59), + body: Consumer( + builder: + (BuildContext context, BotAssistantsState state, Widget? child) => + CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.only(top: 32, bottom: 24), + child: DidvanText( + 'انتخاب بات‌ها', + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xff1B3C59), + ), ), ), ), - switchAssistants(context), - FutureBuilder?>( - future: isMyAssistants - ? BotAssistantsState.getMyAssissmant() - : BotAssistantsState.getGlobalAssissmant(), - builder: (context, snapshot) { - if (snapshot.hasError) { - return const EmptyList(); - } - if (!snapshot.hasData) { - return listOfAssistantsPlaceHolder(); - } - - return listOfAssistants(list: snapshot.data!); - }), - if (isMyAssistants) const SizedBox(height: 72) + if (state.appState != AppState.failed) + SliverToBoxAdapter( + child: switchAssistants(context, state), + ), + SliverStateHandler( + childCount: state.isMyAssistants + ? state.myAssistants.length + : state.globalAssistants.length, + state: state, + emptyState: const EmptyList(), + builder: (context, state, index) { + final assistants = state.isMyAssistants + ? state.myAssistants[index] + : state.globalAssistants[index]; + return assistantsContainer(state, context, assistants); + }, + placeholderCount: 10, + placeholder: assistantsContainerPlaceholder(state, context), + onRetry: state.isMyAssistants + ? state.getMyAssissmant + : state.getGlobalAssissmant), + SliverPadding( + padding: + EdgeInsets.only(bottom: state.isMyAssistants ? 100 : 0)) ], + physics: const BouncingScrollPhysics(), ), ), ); } - ListView listOfAssistants({required final List list}) { - return ListView.builder( - itemCount: list.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(vertical: 8), - itemBuilder: (context, index) { - final assistants = list[index]; - return Container( - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 32), - decoration: const BoxDecoration( - color: Colors.white, borderRadius: DesignConfig.lowBorderRadius), - child: Column( - children: [ - if (isMyAssistants) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.disabledBackground, - borderRadius: DesignConfig.lowBorderRadius), - child: const DidvanText('عمومی'), - ), - ], + Container assistantsContainerPlaceholder( + BotAssistantsState state, BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 32), + decoration: const BoxDecoration( + color: Colors.white, borderRadius: DesignConfig.lowBorderRadius), + child: Column( + children: [ + if (state.isMyAssistants) + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ShimmerPlaceholder( + width: 60, + height: 24, + borderRadius: DesignConfig.lowBorderRadius, ), - Row( - children: [ - SkeletonImage( - imageUrl: assistants.image ?? assistants.bot!.image ?? '', - width: 80, - height: 80, - ), - const SizedBox( - width: 8, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DidvanText( - assistants.name ?? '', - fontSize: 16, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox( - height: 8, - ), - DidvanText( - assistants.description ?? '', - fontSize: 12, - color: Theme.of(context).colorScheme.disabledText, - ), - const SizedBox( - height: 18, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - children: [ - const Icon( - DidvanIcons.calendar_day_light, - size: 18, - ), - const SizedBox( - width: 4, - ), - DidvanText( - DateTime.parse(assistants.createdAt!) - .toPersianDateStr(), - fontSize: 12, - ), - ], - ), - ), - Expanded( - child: isMyAssistants - ? const Row( - children: [ - Icon( - DidvanIcons.user_edit_light, - size: 18, - ), - SizedBox( - width: 4, - ), - DidvanText( - 'ویرایش', - fontSize: 12, - ), - ], - ) - : Row( - children: [ - SkeletonImage( - imageUrl: - assistants.user!.photo ?? '', - width: 24, - height: 24, - borderRadius: - BorderRadius.circular(360), - ), - const SizedBox( - width: 4, - ), - Expanded( - child: DidvanText( - assistants.user!.fullName ?? '', - fontSize: 12, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ) - ], - ), - ], - ), - ) - ], - ), - const SizedBox( - height: 12, - ), - DidvanButton( - title: 'استفاده از دستیار', - onPressed: () => Navigator.pushNamed(context, Routes.aiChat, - arguments: AiChatArgs( - bot: assistants.bot!.copyWith( - id: assistants.id, image: assistants.image), - assistantsName: assistants.name)), - ) - ], - ), - ); - }, - ); - } - - ListView listOfAssistantsPlaceHolder() { - return ListView.builder( - itemCount: 10, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(vertical: 8), - itemBuilder: (context, index) { - return Container( - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 32), - decoration: const BoxDecoration( - color: Colors.white, borderRadius: DesignConfig.lowBorderRadius), - child: Column( + ], + ), + const Row( children: [ - if (isMyAssistants) - const Row( - mainAxisAlignment: MainAxisAlignment.end, + ShimmerPlaceholder( + width: 80, + height: 80, + borderRadius: DesignConfig.lowBorderRadius, + ), + SizedBox( + width: 8, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ShimmerPlaceholder( - width: 60, + width: 120, height: 24, borderRadius: DesignConfig.lowBorderRadius, ), - ], - ), - const Row( - children: [ - ShimmerPlaceholder( - width: 80, - height: 80, - borderRadius: DesignConfig.lowBorderRadius, - ), - SizedBox( - width: 8, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + SizedBox( + height: 8, + ), + ShimmerPlaceholder( + width: 240, + height: 46, + borderRadius: DesignConfig.lowBorderRadius, + ), + SizedBox( + height: 18, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ShimmerPlaceholder( - width: 120, - height: 24, - borderRadius: DesignConfig.lowBorderRadius, + Expanded( + child: ShimmerPlaceholder( + height: 18, + borderRadius: DesignConfig.lowBorderRadius, + ), ), - SizedBox( - height: 8, - ), - ShimmerPlaceholder( - width: 240, - height: 46, - borderRadius: DesignConfig.lowBorderRadius, - ), - SizedBox( - height: 18, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: ShimmerPlaceholder( - height: 18, - borderRadius: DesignConfig.lowBorderRadius, - ), - ), - Expanded(child: SizedBox()), - Expanded( - child: ShimmerPlaceholder( - height: 18, - borderRadius: DesignConfig.lowBorderRadius, - ), - ), - ], + Expanded(child: SizedBox()), + Expanded( + child: ShimmerPlaceholder( + height: 18, + borderRadius: DesignConfig.lowBorderRadius, + ), ), ], ), - ) - ], - ), - const SizedBox( - height: 12, - ), - ShimmerPlaceholder( - width: MediaQuery.sizeOf(context).width, - height: 46, - borderRadius: DesignConfig.lowBorderRadius, - ), + ], + ), + ) ], ), - ); - }, + const SizedBox( + height: 12, + ), + ShimmerPlaceholder( + width: MediaQuery.sizeOf(context).width, + height: 46, + borderRadius: DesignConfig.lowBorderRadius, + ), + ], + ), ); } - Container switchAssistants(BuildContext context) { + Container assistantsContainer(BotAssistantsState state, BuildContext context, + BotAssistants assistants) { + return Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 32), + decoration: const BoxDecoration( + color: Colors.white, borderRadius: DesignConfig.lowBorderRadius), + child: Column( + children: [ + if (state.isMyAssistants) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.disabledBackground, + borderRadius: DesignConfig.lowBorderRadius), + child: DidvanText(assistants.private! ? 'خصوصی' : 'عمومی'), + ), + ], + ), + Row( + children: [ + SkeletonImage( + imageUrl: assistants.image ?? assistants.bot!.image ?? '', + width: 80, + height: 80, + ), + const SizedBox( + width: 8, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + assistants.name ?? '', + fontSize: 16, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox( + height: 8, + ), + DidvanText( + assistants.description ?? + 'dsadsadsadadsadaddadadadsdadsad dsadsadsadadsadaddadadadsdadsad dsadsadsadadsadaddadadadsdadsaddsadsadsadadsadaddadadadsdadsad vdsadsadsadadsadaddadadadsdadsaddsadsadsadadsadaddadadadsdadsaddsadsadsadadsadaddadadadsdadsaddsadsadsadadsadaddadadadsdadsaddsadsadsadadsadaddadadadsdadsaddsadsadsadadsadaddadadadsdadsaddsadsadsadadsadaddadadadsdadsad', + fontSize: 12, + color: Theme.of(context).colorScheme.disabledText, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox( + height: 18, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + const Icon( + DidvanIcons.calendar_day_light, + size: 18, + ), + const SizedBox( + width: 4, + ), + DidvanText( + DateTime.parse(assistants.createdAt!) + .toPersianDateStr(), + fontSize: 12, + ), + ], + ), + ), + Expanded( + child: state.isMyAssistants + ? InkWell( + onTap: () async { + context + .read() + .getAnAssistant(id: assistants.id!); + + Navigator.pushNamed( + context, Routes.createBotAssistants, + arguments: assistants.id); + }, + child: const Row( + children: [ + Icon( + DidvanIcons.user_edit_light, + size: 18, + ), + SizedBox( + width: 4, + ), + DidvanText( + 'ویرایش', + fontSize: 12, + ), + ], + ), + ) + : Row( + children: [ + SkeletonImage( + imageUrl: assistants.user!.photo ?? '', + width: 24, + height: 24, + borderRadius: BorderRadius.circular(360), + ), + const SizedBox( + width: 4, + ), + Expanded( + child: DidvanText( + assistants.user!.fullName ?? '', + fontSize: 12, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ) + ], + ), + ], + ), + ) + ], + ), + const SizedBox( + height: 12, + ), + DidvanButton( + title: 'استفاده از دستیار', + onPressed: () => Navigator.pushNamed(context, Routes.aiChat, + arguments: AiChatArgs( + bot: assistants.bot! + .copyWith(id: assistants.id, image: assistants.image), + assistantsName: assistants.name)), + ) + ], + ), + ); + } + + Container switchAssistants(BuildContext context, BotAssistantsState state) { return Container( margin: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.all(12), @@ -337,12 +343,16 @@ class _BotAssistantsPageState extends State { children: [ Expanded( child: InkWell( - onTap: () => setState(() => isMyAssistants = true), + onTap: () { + state.isMyAssistants = true; + state.getMyAssissmant(); + state.update(); + }, child: Column( children: [ Icon( DidvanIcons.profile_solid, - color: isMyAssistants + color: state.isMyAssistants ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.disabledText, ), @@ -351,14 +361,14 @@ class _BotAssistantsPageState extends State { height: 1, margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: isMyAssistants + color: state.isMyAssistants ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.disabledText, ), ), DidvanText( 'دستیارهای من', - color: isMyAssistants + color: state.isMyAssistants ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.disabledText, ) @@ -375,12 +385,16 @@ class _BotAssistantsPageState extends State { ), Expanded( child: InkWell( - onTap: () => setState(() => isMyAssistants = false), + onTap: () { + state.isMyAssistants = false; + state.getGlobalAssissmant(); + state.update(); + }, child: Column( children: [ Icon( DidvanIcons.profile_solid, - color: !isMyAssistants + color: !state.isMyAssistants ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.disabledText, ), @@ -389,14 +403,14 @@ class _BotAssistantsPageState extends State { height: 1, margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: !isMyAssistants + color: !state.isMyAssistants ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.disabledText, ), ), DidvanText( 'دستیارهای دیگران', - color: !isMyAssistants + color: !state.isMyAssistants ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.disabledText, ) diff --git a/lib/views/ai/bot_assistants_state.dart b/lib/views/ai/bot_assistants_state.dart index 3542929..ee7b53f 100644 --- a/lib/views/ai/bot_assistants_state.dart +++ b/lib/views/ai/bot_assistants_state.dart @@ -1,28 +1,42 @@ import 'package:didvan/models/ai/bot_assistants_model.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/providers/core.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; -class BotAssistantsState { - static Future?> getGlobalAssissmant() async { - List? globalAssissmant; +class BotAssistantsState extends CoreProvier { + List myAssistants = []; + List globalAssistants = []; + bool isMyAssistants = true; + void getGlobalAssissmant() async { + globalAssistants.clear(); + + appState = AppState.busy; + update(); final service = RequestService( - RequestHelper.usersAssistants(), + RequestHelper.usersAssistants(personal: false), ); await service.httpGet(); if (service.isSuccess) { final BotAssistantsModel toolsModel = BotAssistantsModel.fromJson(service.result); - globalAssissmant = toolsModel.botAssistants!; - return globalAssissmant; + + globalAssistants.addAll(toolsModel.botAssistants ?? []); + + appState = AppState.idle; + update(); + return; } - throw 'err'; + appState = AppState.failed; + update(); } - static Future?> getMyAssissmant() async { - List? globalAssissmant; - + void getMyAssissmant() async { + myAssistants.clear(); + appState = AppState.busy; + update(); final service = RequestService( RequestHelper.usersAssistants(personal: true), ); @@ -30,9 +44,14 @@ class BotAssistantsState { if (service.isSuccess) { final BotAssistantsModel toolsModel = BotAssistantsModel.fromJson(service.result); - globalAssissmant = toolsModel.botAssistants!; - return globalAssissmant; + + myAssistants.addAll(toolsModel.botAssistants ?? []); + + appState = AppState.idle; + update(); + return; } - throw 'err'; + appState = AppState.failed; + update(); } } diff --git a/lib/views/ai/create_bot_assistants_page.dart b/lib/views/ai/create_bot_assistants_page.dart index 2c8ffd0..98ae829 100644 --- a/lib/views/ai/create_bot_assistants_page.dart +++ b/lib/views/ai/create_bot_assistants_page.dart @@ -1,12 +1,19 @@ +// ignore_for_file: deprecated_member_use + +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_model.dart'; +import 'package:didvan/models/ai/bot_assistants_req_model.dart'; import 'package:didvan/models/ai/bots_model.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/action_sheet_data.dart'; +import 'package:didvan/services/media/media.dart'; import 'package:didvan/utils/action_sheet.dart'; +import 'package:didvan/views/ai/bot_assistants_state.dart'; import 'package:didvan/views/ai/create_bot_assistants_state.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/widgets/didvan/button.dart'; @@ -19,10 +26,13 @@ import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; class CreateBotAssistantsPage extends StatefulWidget { - const CreateBotAssistantsPage({Key? key}) : super(key: key); + final int? id; + const CreateBotAssistantsPage({Key? key, this.id}) : super(key: key); @override State createState() => @@ -30,20 +40,31 @@ class CreateBotAssistantsPage extends StatefulWidget { } class _CreateBotAssistantsPageState extends State { - final List botModels = ['مدل زبانی', 'مدل تصویری']; - late List allBots = context.read().bots; - final _formYouTubeKey = GlobalKey(); - final _formNameKey = GlobalKey(); - final _formDescKey = GlobalKey(); - - List countOfLink = ['']; - int selectedItem = 0; - bool inEdite = true; - late InputBorder defaultBorder = OutlineInputBorder( borderRadius: DesignConfig.mediumBorderRadius, borderSide: BorderSide( width: 1, color: Theme.of(context).colorScheme.disabledText)); + late List allBots = context.read().bots; + + final List botModels = ['مدل زبانی', 'مدل تصویری']; + + // final _formYouTubeKey = GlobalKey(); + final _formNameKey = GlobalKey(); + final _formPromptKey = GlobalKey(); + final _formDescKey = GlobalKey(); + + String name = ''; + String prompt = ''; + String desc = ''; + // String? youtubeLink; + + List countOfLink = ['']; + List files = []; + ValueNotifier image = ValueNotifier(null); + String selectedBotType = 'text'; + bool isPrivate = true; + BotAssistants? assistant; + BotsModel? initialBot; @override void initState() { @@ -52,401 +73,614 @@ class _CreateBotAssistantsPageState extends State { context.read().getImageToolsBots(); } + void onConfirm(CreateBotAssistantsState state, int selectedBotId) 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; + } + await state.createAssistants( + id: widget.id, + data: BotAssistantsReqModel( + type: selectedBotType, + name: name, + botId: selectedBotId, + prompt: prompt, + webLinks: countOfLink.first.isEmpty ? null : countOfLink, + // youtubeLink: youtubeLink, + isPrivate: isPrivate, + files: files, + image: image.value)); + + context.read().getMyAssissmant(); + Navigator.pop(context); + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: HoshanAppBar( - onBack: () => Navigator.pop(context), - withActions: false, - ), - body: Consumer( - builder: (BuildContext context, CreateBotAssistantsState state, - Widget? child) => - 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( - closedHeaderPadding: const EdgeInsets.all(12), - items: botModels, - initialItem: botModels[selectedItem], - hideSelectedFieldWhenExpanded: false, - decoration: CustomDropdownDecoration( - listItemDecoration: ListItemDecoration( - selectedColor: Theme.of(context) - .colorScheme - .surface - .withOpacity(0.5), - ), - closedBorder: Border.all(color: Colors.grey), - closedFillColor: Colors.grey.shade100.withOpacity(0.1), - expandedFillColor: - Theme.of(context).colorScheme.surface), - // hintText: "انتخاب کنید", - onChanged: (value) { - setState(() { - selectedItem = botModels.indexOf(value!); - }); - }, + int selectedBotId = selectedBotType == 'text' + ? allBots.first.id! + : context.read().imageBots.first.id!; + + return WillPopScope( + onWillPop: () async { + context.read().assistant = null; + + return true; + }, + child: Scaffold( + appBar: HoshanAppBar( + onBack: () => Navigator.pop(context), + withActions: false, + ), + body: Consumer(builder: (BuildContext context, + CreateBotAssistantsState state, Widget? child) { + assistant = state.assistant; + if (assistant != null) { + name = assistant!.name ?? ''; + prompt = assistant!.prompt ?? ''; + desc = assistant!.description ?? ''; + // youtubeLink = assistant!.; + isPrivate = assistant!.private ?? true; + if (assistant!.files != null && assistant!.files!.isNotEmpty) { + for (var file in assistant!.files!) { + final data = File.fromUri(Uri.parse(file)).readAsBytesSync(); + files.add(XFile.fromData(data)); + } + } + countOfLink = assistant!.websites ?? ['']; + selectedBotType = assistant!.type ?? 'text'; + + final list = selectedBotType == 'text' ? allBots : state.imageBots; + for (var bot in list) { + if (bot.id == assistant!.botId) { + initialBot = bot; + break; + } + } + } + + return state.loading + ? Center( + child: SpinKitThreeBounce( + size: 46, + color: Theme.of(context).colorScheme.primary, ), - ), - const SizedBox( - height: 24, - ), - const Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - DidvanButton( - width: 120, - style: ButtonStyleMode.flat, - title: 'انتخاب عکس', - ), - SkeletonImage( - imageUrl: 'https://via.placeholder.com/70x70', - width: 80, - height: 80, - ), - ], - ), - const SizedBox( - height: 24, - ), - title(text: 'انتخاب نام'), - Form( - key: _formNameKey, - child: DidvanTextField( - onChanged: (value) {}, - validator: (value) { - String? result; - if (value.isEmpty) { - result = 'نام نباید خالی باشد'; - } else if (value.length < 4) { - result = 'نام نباید کمتر از 4 حرف باشد'; - } - return result; - }, - hintText: 'ai@2024_B', - maxLength: 20, - ), - ), - 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( - 'نام منحصر به فرد شامل 4 تا 20 کاراکتر (حروف، اعداد، خط تیره، نقطه و زیرخط) ', - textAlign: TextAlign.right, - fontSize: 12, - color: Theme.of(context).colorScheme.caption, - ), - ), - ], - ), - 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( - closedHeaderPadding: const EdgeInsets.all(12), - items: selectedItem == 0 ? allBots : state.imageBots, - headerBuilder: (context, bot, enabled) => - botRow(bot, context), - listItemBuilder: - (context, bot, isSelected, onItemSelect) => - botRow(bot, context), - initialItem: - (selectedItem == 0 ? allBots : state.imageBots) - .first, - hideSelectedFieldWhenExpanded: false, - decoration: CustomDropdownDecoration( - listItemDecoration: ListItemDecoration( - selectedColor: Theme.of(context) - .colorScheme - .surface - .withOpacity(0.5), - ), - closedBorder: Border.all(color: Colors.grey), - closedFillColor: - Colors.grey.shade100.withOpacity(0.1), - expandedFillColor: - Theme.of(context).colorScheme.surface), - // hintText: "انتخاب کنید", - onChanged: (value) { - // setState(() { - // selectedItem = value; - // }); - }, - ), - ), - const SizedBox( - height: 24, - ), - title(text: 'دستورالعمل'), - Form( - key: _formDescKey, - child: DidvanTextField( - hintText: - 'به ربات خود بگویید که چگونه رفتار کند و چگونه به پیام‌های کاربر پاسخ دهد. سعی کنید تا حد امکان واضح و مشخص باشید.', - textInputType: TextInputType.multiline, - minLine: 6, - maxLine: 6, - maxLength: 400, - hasHeight: false, - showLen: true, - validator: (value) { - String? result; - if (value.isEmpty) { - result = 'دستورالعمل نباید خالی باشد'; - } else if (value.length < 10) { - result = 'نام نباید کمتر از 10 حرف باشد'; - } - return result; - }, - ), - ), - const SizedBox( - height: 24, - ), - if (selectedItem == 0) - Column( - children: [ - title(text: 'پایگاه دانش', isRequired: false), - SizedBox( - height: 48, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context) - .colorScheme - .disabledBackground, - shape: const RoundedRectangleBorder( - borderRadius: - DesignConfig.lowBorderRadius)), - onPressed: () {}, - 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, - ) - ], - )), - ), - const SizedBox( - height: 24, - ), - ], - ), - title(text: 'لینک یوتیوب', isRequired: false), - Form( - key: _formYouTubeKey, - child: DidvanTextField( - onChanged: (value) {}, - validator: (value) => - value.startsWith('https://www.youtube.com') - ? null - : 'باید لینک یوتیوب باشد', - hintText: 'https://www.youtube.com/watch?v', - ), - ), - const SizedBox( - height: 24, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - title(text: 'لینک وب سایت', isRequired: false), - Row( + ) + : SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20.0, vertical: 32), + child: Column( children: [ - if (countOfLink.length > 1) - DidvanIconButton( - icon: CupertinoIcons.minus_circle_fill, - onPressed: () { + title(text: 'نوع دستیار'), + SizedBox( + width: MediaQuery.sizeOf(context).width, + child: CustomDropdown( + closedHeaderPadding: const EdgeInsets.all(12), + items: botModels, + initialItem: botModels[0], + hideSelectedFieldWhenExpanded: false, + decoration: CustomDropdownDecoration( + listItemDecoration: ListItemDecoration( + selectedColor: Theme.of(context) + .colorScheme + .surface + .withOpacity(0.5), + ), + closedBorder: Border.all(color: Colors.grey), + closedFillColor: + Colors.grey.shade100.withOpacity(0.1), + expandedFillColor: + Theme.of(context).colorScheme.surface), + // hintText: "انتخاب کنید", + onChanged: (value) { setState(() { - countOfLink.removeLast(); + final index = botModels.indexOf(value!); + selectedBotType = index == 0 ? 'text' : 'image'; }); }, ), - DidvanIconButton( - icon: CupertinoIcons.plus_circle_fill, - onPressed: () { - setState(() { - countOfLink.add(''); - }); - }, ), - ], - ) - ], - ), - ListView.builder( - shrinkWrap: true, - itemCount: countOfLink.length, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => Column( - children: [ - DidvanTextField( - onChanged: (value) { - countOfLink.insert(index, value); - }, - // 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, - ), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const DidvanText( - 'نمایش عمومی', - fontSize: 14, + const SizedBox( + height: 24, + ), + ValueListenableBuilder( + valueListenable: image, + builder: (context, img, _) { + return Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + DidvanButton( + width: 120, + style: ButtonStyleMode.flat, + color: image.value != null || + assistant?.image != null + ? Theme.of(context).colorScheme.error + : null, + title: image.value != null || + assistant?.image != null + ? 'حذف عکس' + : 'انتخاب عکس', + onPressed: () async { + if (image.value != null || + assistant?.image != null) { + if (assistant != null) { + assistant!.image = null; + } + image.value = null; + return; + } + image.value = + await MediaService.pickImage( + source: ImageSource.gallery); + }, + ), + img != null + ? ClipRRect( + borderRadius: + DesignConfig.lowBorderRadius, + child: SizedBox( + width: 80, + height: 80, + child: + Image.file(File(img.path))), + ) + : SkeletonImage( + imageUrl: assistant != null && + assistant!.image != null + ? assistant!.image! + : 'https://via.placeholder.com/70x70', + width: 80, + height: 80, + ) + ], + ); + }), + const SizedBox( + height: 24, + ), + title(text: 'انتخاب نام'), + Form( + key: _formNameKey, + child: DidvanTextField( + initialValue: name, + onChanged: (value) { + name = value; + }, + validator: (value) { + String? result; + if (value.isEmpty) { + result = 'نام نباید خالی باشد'; + } else if (value.length < 4) { + result = 'نام نباید کمتر از 4 حرف باشد'; + } + return result; + }, + hintText: 'ai@2024_B', + maxLength: 20, ), - Row( - children: [ - Expanded( - child: DidvanText( - 'در صورت فعال بودن، دستیار شما توسط سایرین قابل مشاهده بوده و مورد استفاده قرار می‌گیرد.', - fontSize: 14, - color: Theme.of(context).colorScheme.caption, + ), + 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( + 'نام منحصر به فرد شامل 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: desc, + // hintText: + // 'به ربات خود بگویید که چگونه رفتار کند و چگونه به پیام‌های کاربر پاسخ دهد. سعی کنید تا حد امکان واضح و مشخص باشید.', + textInputType: TextInputType.multiline, + minLine: 4, + maxLine: 4, + maxLength: 200, + hasHeight: false, + showLen: true, + onChanged: (value) { + 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( + closedHeaderPadding: const EdgeInsets.all(12), + items: selectedBotType == 'text' + ? allBots + : state.imageBots, + headerBuilder: (context, bot, enabled) => + botRow(bot, context), + listItemBuilder: (context, bot, isSelected, + onItemSelect) => + botRow(bot, context), + initialItem: assistant != null + ? initialBot + : (selectedBotType == 'text' + ? allBots + : state.imageBots) + .first, + hideSelectedFieldWhenExpanded: false, + decoration: CustomDropdownDecoration( + listItemDecoration: ListItemDecoration( + selectedColor: Theme.of(context) + .colorScheme + .surface + .withOpacity(0.5), + ), + closedBorder: + Border.all(color: Colors.grey), + closedFillColor: + Colors.grey.shade100.withOpacity(0.1), + expandedFillColor: Theme.of(context) + .colorScheme + .surface), + // hintText: "انتخاب کنید", + onChanged: (value) { + setState(() { + selectedBotId = value!.id!; + }); + }, ), ), + + const SizedBox( + height: 24, + ), + title(text: 'دستورالعمل'), + Form( + key: _formPromptKey, + child: DidvanTextField( + initialValue: prompt, + hintText: + 'به ربات خود بگویید که چگونه رفتار کند و چگونه به پیام‌های کاربر پاسخ دهد. سعی کنید تا حد امکان واضح و مشخص باشید.', + textInputType: TextInputType.multiline, + minLine: 6, + maxLine: 6, + maxLength: 400, + hasHeight: false, + showLen: true, + onChanged: (value) { + prompt = value; + }, + validator: (value) { + String? result; + if (value.isEmpty) { + result = 'دستورالعمل نباید خالی باشد'; + } else if (value.length < 10) { + result = 'نام نباید کمتر از 10 حرف باشد'; + } + return result; + }, + ), + ), + const SizedBox( + height: 24, + ), + if (selectedBotType == 'text') + Column( + children: [ + title(text: 'پایگاه دانش', isRequired: false), + 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) { + files.addAll(picks.xFiles); + } + }, + 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, + ) + ], + )), + ), + const SizedBox( + height: 24, + ), ], - ) - ], - ), - ), - SizedBox( - width: 64, - height: 48, - child: DidvanSwitch( - value: false, - title: '', - onChanged: (value) {}, - ), - ), - ], - ), - const SizedBox( - height: 24, - ), - inEdite - ? Flex( - direction: Axis.horizontal, - children: [ - Flexible( - flex: 2, - child: DidvanButton( - title: 'ذخیره تغییرات', - onPressed: () { - final List valid = []; - valid.add( - !_formYouTubeKey.currentState!.validate()); - valid.add( - !_formNameKey.currentState!.validate()); - valid.add( - !_formDescKey.currentState!.validate()); - if (valid.firstWhere( - (element) => !element, - )) return; - }, + ), + // 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( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + title(text: 'لینک وب سایت', isRequired: false), + Row( + children: [ + if (countOfLink.length > 1) + DidvanIconButton( + icon: CupertinoIcons.minus_circle_fill, + onPressed: () { + setState(() { + countOfLink.removeLast(); + }); + }, + ), + if (countOfLink.length != 3) + DidvanIconButton( + icon: CupertinoIcons.plus_circle_fill, + onPressed: () { + setState(() { + if (countOfLink.last.isNotEmpty) { + countOfLink.add(''); + } + }); + }, + ), + ], + ) + ], + ), + ListView.builder( + shrinkWrap: true, + itemCount: countOfLink.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => Column( + children: [ + DidvanTextField( + onChanged: (value) { + countOfLink.insert(index, value); + }, + // 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: 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, - ) - ], - ))); - }, + const SizedBox(width: 4), + Expanded( + child: DidvanText( + 'دستیار شما با استناد بر اطلاعات ارائه شده در پایگاه دانش، پیام کاربران را ارزیابی می‌کند.', + textAlign: TextAlign.right, + fontSize: 12, + color: Theme.of(context).colorScheme.caption, + ), ), - ), - ], - ) - : const DidvanButton( - title: 'ذخیره', - ), - const SizedBox( - height: 24, - ), - ], - ), - ), - ), + ], + ), + 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: !isPrivate, + title: '', + onChanged: (value) { + 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, selectedBotId), + ), + 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 { + await state.deleteAssistants( + id: widget.id!); + Navigator.pop(context); + }, + )); + }, + ), + ), + ], + ) + : Column( + children: [ + DidvanButton( + title: state.loadingCreate ? '' : 'ذخیره', + onPressed: () async => + onConfirm(state, selectedBotId)), + if (state.loadingCreate) + const Positioned.fill( + child: Center( + child: SpinKitThreeBounce( + size: 32, + color: Colors.white, + ), + ), + ) + ], + ), + const SizedBox( + height: 24, + ), + ], + ), + ), + ); + }), ), ); } @@ -456,12 +690,11 @@ class _CreateBotAssistantsPageState extends State { alignment: Alignment.center, child: Row( children: [ - ClipOval( - child: CachedNetworkImage( - imageUrl: bot.image.toString(), - width: 42, - height: 42, - ), + SkeletonImage( + imageUrl: bot.image.toString(), + width: 42, + height: 42, + borderRadius: BorderRadius.circular(360), ), const SizedBox(width: 12), Expanded( diff --git a/lib/views/ai/create_bot_assistants_state.dart b/lib/views/ai/create_bot_assistants_state.dart index 8cb15c6..b512fae 100644 --- a/lib/views/ai/create_bot_assistants_state.dart +++ b/lib/views/ai/create_bot_assistants_state.dart @@ -1,3 +1,5 @@ +import 'package:didvan/models/ai/bot_assistants_model.dart'; +import 'package:didvan/models/ai/bot_assistants_req_model.dart'; import 'package:didvan/models/ai/bots_model.dart'; import 'package:didvan/models/ai/tools_model.dart'; import 'package:didvan/models/enums.dart'; @@ -8,8 +10,13 @@ import 'package:didvan/services/network/request_helper.dart'; class CreateBotAssistantsState extends CoreProvier { List imageBots = []; bool loadingImageBots = false; + bool loadingCreate = false; + bool loading = false; + BotAssistants? assistant; void getImageToolsBots() async { + loadingImageBots = true; + final service = RequestService( RequestHelper.tools(), ); @@ -32,5 +39,65 @@ class CreateBotAssistantsState extends CoreProvier { update(); } - void createAssistants() {} + Future createAssistants( + {required final BotAssistantsReqModel data, final int? id}) async { + loadingCreate = true; + update(); + + final service = RequestService( + (id != null + ? RequestHelper.updateAssistants(id) + : RequestHelper.createAssistants()), + body: data.toJson()); + await service.multipartFilesCreateAssismants( + files: data.files, + image: data.image, + method: id != null ? 'PUT' : 'POST'); + if (service.isSuccess) { + appState = AppState.idle; + loadingCreate = false; + update(); + return; + } + appState = AppState.failed; + loadingCreate = false; + update(); + } + + Future getAnAssistant({required final int id}) async { + loading = true; + update(); + + final service = RequestService( + RequestHelper.getAssistant(id), + ); + await service.httpGet(); + if (service.isSuccess) { + assistant = BotAssistants.fromJson(service.result['bot']); + appState = AppState.idle; + loading = false; + update(); + return; + } + appState = AppState.failed; + loading = false; + update(); + } + + Future deleteAssistants({required final int id}) async { + loadingCreate = true; + update(); + + final service = RequestService(RequestHelper.getAssistant(id)); + await service.delete(); + if (service.isSuccess) { + appState = AppState.idle; + loadingCreate = false; + update(); + return; + } + appState = AppState.failed; + loadingCreate = false; + update(); + } } diff --git a/lib/views/ai/history_ai_chat_page.dart b/lib/views/ai/history_ai_chat_page.dart index aa78ae8..8c70262 100644 --- a/lib/views/ai/history_ai_chat_page.dart +++ b/lib/views/ai/history_ai_chat_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; -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'; @@ -19,6 +18,7 @@ import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/search_field.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; @@ -65,6 +65,7 @@ class _HistoryAiChatPageState extends State { // floatingActionButton: openAiListBtn(context), padding: EdgeInsets.zero, scrollController: scrollController, + showSliversFirst: false, slivers: [ SliverAppBar( @@ -258,14 +259,11 @@ class _HistoryAiChatPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - SizedBox( + SkeletonImage( + imageUrl: chat.bot!.image.toString(), width: 46, height: 46, - child: ClipOval( - child: CachedNetworkImage( - imageUrl: chat.bot!.image.toString(), - ), - ), + borderRadius: BorderRadius.circular(360), ), const SizedBox( width: 18, diff --git a/lib/views/ai/history_ai_chat_state.dart b/lib/views/ai/history_ai_chat_state.dart index 5425f7f..1c30287 100644 --- a/lib/views/ai/history_ai_chat_state.dart +++ b/lib/views/ai/history_ai_chat_state.dart @@ -48,6 +48,8 @@ class HistoryAiChatState extends CoreProvier { Future getSearchChats( {required final String q, final bool archived = false}) async { + appState = AppState.busy; + update(); final service = RequestService( archived ? RequestHelper.aiSearchArchived(q) diff --git a/lib/views/ai/widgets/hoshan_drawer.dart b/lib/views/ai/widgets/hoshan_drawer.dart index 149b5a6..71203d0 100644 --- a/lib/views/ai/widgets/hoshan_drawer.dart +++ b/lib/views/ai/widgets/hoshan_drawer.dart @@ -1,4 +1,3 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; @@ -15,6 +14,7 @@ import 'package:didvan/views/home/home.dart'; import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/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/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -358,12 +358,12 @@ class _HoshanDrawerState extends State { }, child: Row( children: [ - ClipOval( - child: CachedNetworkImage( + SkeletonImage( imageUrl: chat.bot!.image.toString(), width: 24, height: 24, - )), + borderRadius: BorderRadius.circular(360), + ), const SizedBox( width: 12, ), diff --git a/lib/views/widgets/hoshan_app_bar.dart b/lib/views/widgets/hoshan_app_bar.dart index cc37249..f434ae4 100644 --- a/lib/views/widgets/hoshan_app_bar.dart +++ b/lib/views/widgets/hoshan_app_bar.dart @@ -4,6 +4,7 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/ai/ai_state.dart'; +import 'package:didvan/views/ai/bot_assistants_state.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; @@ -63,6 +64,8 @@ class HoshanAppBar extends StatelessWidget implements PreferredSizeWidget { icon: DidvanIcons.antenna_light, size: 32, onPressed: () { + context.read().getMyAssissmant(); + Navigator.pushNamed(context, Routes.botAssistants); }, ),