diff --git a/lib/assets/icons/clarity_tools-line.svg b/lib/assets/icons/clarity_tools-line.svg new file mode 100644 index 0000000..948e32b --- /dev/null +++ b/lib/assets/icons/clarity_tools-line.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/lib/assets/icons/create image.svg b/lib/assets/icons/create image.svg new file mode 100644 index 0000000..8cb46cb --- /dev/null +++ b/lib/assets/icons/create image.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/assets/icons/global-search 2.svg b/lib/assets/icons/global-search 2.svg new file mode 100644 index 0000000..52218bd --- /dev/null +++ b/lib/assets/icons/global-search 2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/lib/assets/icons/message-question.svg b/lib/assets/icons/message-question.svg new file mode 100644 index 0000000..56d84db --- /dev/null +++ b/lib/assets/icons/message-question.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/microphone-3.svg b/lib/assets/icons/microphone-3.svg new file mode 100644 index 0000000..a696032 --- /dev/null +++ b/lib/assets/icons/microphone-3.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/assets/icons/send3.svg b/lib/assets/icons/send3.svg new file mode 100644 index 0000000..eb076e5 --- /dev/null +++ b/lib/assets/icons/send3.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/summary.svg b/lib/assets/icons/summary.svg new file mode 100644 index 0000000..d42013a --- /dev/null +++ b/lib/assets/icons/summary.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/translate.svg b/lib/assets/icons/translate.svg new file mode 100644 index 0000000..6867825 --- /dev/null +++ b/lib/assets/icons/translate.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index 3713923..c8eeca9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,7 +50,7 @@ Future _backgroundCallbackHomeWidget(Uri? uri) async { void main() async { runZonedGuarded( - () async { + () async { WidgetsFlutterBinding.ensureInitialized(); try { if (!kIsWeb) { @@ -73,7 +73,7 @@ void main() async { } await SentryFlutter.init( - (options) { + (options) { options.dsn = 'https://a4cfcaa7d67471240d295c25c968d91d@o4508585857384448.ingest.de.sentry.io/4508585886548048'; options.tracesSampleRate = 1.0; options.profilesSampleRate = 1.0; @@ -81,7 +81,7 @@ void main() async { appRunner: () => runApp(const Didvan()), ); }, - (error, stack) { + (error, stack) { Sentry.captureException(error, stackTrace: stack); }, ); @@ -178,7 +178,10 @@ class _DidvanState extends State with WidgetsBindingObserver { providers: [ ChangeNotifierProvider(create: (context) => PodcastsState()), ChangeNotifierProvider(create: (context) => MediaProvider()), + // --- MODIFIED --- + // حذف ..fetchWelcomeMessage() ChangeNotifierProvider(create: (context) => UserProvider()), + // --- END MODIFIED --- ChangeNotifierProvider(create: (context) => ThemeProvider()), ChangeNotifierProvider(create: (context) => StudioDetailsState()), ChangeNotifierProvider(create: (context) => HistoryAiChatState()), @@ -190,42 +193,43 @@ class _DidvanState extends State with WidgetsBindingObserver { builder: (context, themeProvider, child) => Container( color: Theme.of(context).colorScheme.surface, child: SafeArea( - child: MaterialApp( - scrollBehavior: MyCustomScrollBehavior(), - navigatorKey: navigatorKey, - debugShowCheckedModeBanner: false, - title: 'Didvan', - theme: LightThemeConfig.themeData.copyWith( - bottomSheetTheme: const BottomSheetThemeData( - surfaceTintColor: Colors.transparent, - backgroundColor: Colors.transparent), - textTheme: LightThemeConfig.themeData.textTheme.apply( - fontFamily: themeProvider.fontFamily, - )), - darkTheme: DarkThemeConfig.themeData.copyWith( - bottomSheetTheme: const BottomSheetThemeData( - surfaceTintColor: Colors.transparent, - backgroundColor: Colors.transparent), - textTheme: DarkThemeConfig.themeData.textTheme.apply( - fontFamily: themeProvider.fontFamily, - )), - color: LightThemeConfig.themeData.primaryColor, - themeMode: themeProvider.themeMode, - onGenerateRoute: (settings) => - RouteGenerator.generateRoute(settings), - builder: BotToastInit(), - navigatorObservers: [BotToastNavigatorObserver()], - initialRoute: "/", - localizationsDelegates: const [ - GlobalCupertinoLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - supportedLocales: const [ - Locale("fa", "IR"), - ], - locale: const Locale("fa", "IR"), - )), + child: MaterialApp( + scrollBehavior: MyCustomScrollBehavior(), + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + title: 'Didvan', + theme: LightThemeConfig.themeData.copyWith( + bottomSheetTheme: const BottomSheetThemeData( + surfaceTintColor: Colors.transparent, + backgroundColor: Colors.transparent), + textTheme: LightThemeConfig.themeData.textTheme.apply( + fontFamily: themeProvider.fontFamily, + )), + darkTheme: DarkThemeConfig.themeData.copyWith( + bottomSheetTheme: const BottomSheetThemeData( + surfaceTintColor: Colors.transparent, + backgroundColor: Colors.transparent), + textTheme: DarkThemeConfig.themeData.textTheme.apply( + fontFamily: themeProvider.fontFamily, + )), + color: LightThemeConfig.themeData.primaryColor, + themeMode: themeProvider.themeMode, + onGenerateRoute: (settings) => + RouteGenerator.generateRoute(settings), + builder: BotToastInit(), + navigatorObservers: [BotToastNavigatorObserver()], + initialRoute: "/", + localizationsDelegates: const [ + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: const [ + Locale("fa", "IR"), + ], + locale: const Locale("fa", "IR"), + ), + ), ), ), ); diff --git a/lib/models/ai/ai_model_enum.dart b/lib/models/ai/ai_model_enum.dart new file mode 100644 index 0000000..1155a39 --- /dev/null +++ b/lib/models/ai/ai_model_enum.dart @@ -0,0 +1,7 @@ +// lib/models/ai/ai_model_enum.dart + +enum AiModel { + chatGPT, + gemini, + grok +} diff --git a/lib/providers/user.dart b/lib/providers/user.dart index 464412a..3e476d8 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -16,6 +16,14 @@ class UserProvider extends CoreProvier { bool isAuthenticated = false; int _unreadMessageCount = 0; + // --- ADDED --- + String? _welcomeMessage; + bool _isLoadingWelcome = true; + + String? get welcomeMessage => _welcomeMessage; + bool get isLoadingWelcome => _isLoadingWelcome; + // --- END ADDED --- + set unreadMessageCount(int value) { if (value < 0) { return; @@ -32,6 +40,55 @@ class UserProvider extends CoreProvier { static final List _statisticMarkQueue = []; static final List _itemMarkQueue = []; + // --- ADDED --- + Future fetchWelcomeMessage() async { + if (!_isLoadingWelcome) { + _isLoadingWelcome = true; + notifyListeners(); // برای نمایش لودینگ اگر قبلا مخفی شده بود + } + + try { + const String url = 'https://api.didvan.app/ai/aiwellcom'; + // اطمینان از اینکه توکن وجود دارد قبل از ارسال درخواست + if (RequestService.token == null) { + print("UserProvider: fetchWelcomeMessage skipped, token is null."); + _isLoadingWelcome = false; // توکن نیست، لودینگ تمام + notifyListeners(); + return; + } + print("UserProvider: Fetching welcome message..."); // Log start + + final service = RequestService(url, useAutherization: true); + await service.post(); + + if (service.isSuccess) { + print("UserProvider: Welcome message API success."); // Log success + final period = service.data('period'); + final userData = service.data('user'); // Rename to avoid conflict with class member 'user' + if (period != null && userData is Map && userData.containsKey('fullName')) { + final fullName = userData['fullName']; + _welcomeMessage = '$period بخیر $fullName 👋'; + print("UserProvider: Welcome message set: $_welcomeMessage"); // Log message content + } else { + print("UserProvider: Welcome message API success but data format unexpected."); + _welcomeMessage = null; // Clear if data format is wrong + } + } else { + print("UserProvider: Welcome message API failed. Status: ${service.statusCode}, Error: ${service.errorMessage}"); // Log failure + _welcomeMessage = null; // Clear on API error + } + } catch (e) { + print("UserProvider: Exception fetching welcome message: $e"); // Log exception + _welcomeMessage = null; // Clear on exception + } finally { + // همیشه لودینگ رو false کن و UI رو آپدیت کن + _isLoadingWelcome = false; + print("UserProvider: fetchWelcomeMessage finished. isLoadingWelcome: $_isLoadingWelcome"); // Log end + notifyListeners(); + } + } + // --- END ADDED --- + Future setAndGetToken({String? newToken}) async { try { if (newToken == null) { @@ -46,15 +103,25 @@ class UserProvider extends CoreProvier { } Future getUserInfo() async { - isAuthenticated = true; + isAuthenticated = true; // Assume authenticated until proven otherwise + print("UserProvider: Getting user info..."); // Log start final RequestService service = RequestService(RequestHelper.userInfo); await service.httpGet(); - if (service.statusCode == 401 || - (service.isSuccess && service.result['user'] == null)) { + + if (service.statusCode == 401) { + print("UserProvider: getUserInfo failed - Unauthorized (401)."); + isAuthenticated = false; // Not authenticated return false; } + if (service.isSuccess) { + if (service.result['user'] == null) { + print("UserProvider: getUserInfo success but user data is null."); + isAuthenticated = false; // Treat as not authenticated if user data is null + return false; + } try { + print("UserProvider: User info fetched successfully."); // Log success user = User.fromJson(service.result['user']); await StorageService.setValue( key: 'notificationTimeRangeStart', @@ -65,17 +132,30 @@ class UserProvider extends CoreProvier { value: service.result['user']['end'], ); + // توکن فایربیس رو ثبت کن await _registerFirebaseToken(); - notifyListeners(); + notifyListeners(); // اول اطلاعات کاربر رو آپدیت کن + + // --- ADDED --- + // حالا که اطلاعات کاربر و توکن لود شده، پیام خوشامدگویی رو بگیر + await fetchWelcomeMessage(); + // --- END ADDED --- return true; } catch (e) { + print("UserProvider: Exception processing user info: $e"); + isAuthenticated = false; // Error processing, assume not authenticated return false; } } - throw 'Getting user from API failed!'; + // If service failed for reasons other than 401 + print("UserProvider: getUserInfo failed. Status: ${service.statusCode}, Error: ${service.errorMessage}"); + isAuthenticated = false; // Failed to get user info + // Consider throwing an error or handling it based on app logic + // throw 'Getting user from API failed!'; // Or return false + return false; } Future _registerFirebaseToken() async { @@ -221,51 +301,53 @@ class UserProvider extends CoreProvier { } // static Future changeRadarMark(int id, bool value) async { - // _radarMarkQueue.add(MapEntry(id, value)); - // Future.delayed(const Duration(milliseconds: 500), () async { - // final MapEntry? lastChange = - // _radarMarkQueue.lastWhereOrNull((item) => item.key == id); - // if (lastChange == null) return; - // final service = RequestService(RequestHelper.mark(id, 'radar')); - // if (lastChange.value) { - // await service.post(); - // } else { - // await service.delete(); - // } - // _radarMarkQueue.removeWhere((element) => element.key == id); - // }); + //   _radarMarkQueue.add(MapEntry(id, value)); + //   Future.delayed(const Duration(milliseconds: 500), () async { + //     final MapEntry? lastChange = + //         _radarMarkQueue.lastWhereOrNull((item) => item.key == id); + //     if (lastChange == null) return; + //     final service = RequestService(RequestHelper.mark(id, 'radar')); + //     if (lastChange.value) { + //       await service.post(); + //     } else { + //       await service.delete(); + //     } + //     _radarMarkQueue.removeWhere((element) => element.key == id); + //   }); // } // static Future changeStudioMark(int id, bool value) async { - // _studioMarkQueue.add(MapEntry(id, value)); - // Future.delayed(const Duration(milliseconds: 500), () async { - // final MapEntry? lastChange = - // _studioMarkQueue.lastWhereOrNull((item) => item.key == id); - // if (lastChange == null) return; - // final service = RequestService(RequestHelper.mark(id, 'studio')); - // if (lastChange.value) { - // await service.post(); - // } else { - // await service.delete(); - // } - // _studioMarkQueue.removeWhere((element) => element.key == id); - // }); + //   _studioMarkQueue.add(MapEntry(id, value)); + //   Future.delayed(const Duration(milliseconds: 500), () async { + //     final MapEntry? lastChange = + //         _studioMarkQueue.lastWhereOrNull((item) => item.key == id); + //     if (lastChange == null) return; + //     final service = RequestService(RequestHelper.mark(id, 'studio')); + //     if (lastChange.value) { + //       await service.post(); + //     } else { + //       await service.delete(); + //     } + //     _studioMarkQueue.removeWhere((element) => element.key == id); + //   }); // } // static Future changeNewsMark(int id, bool value) async { - // _newsMarkQueue.add(MapEntry(id, value)); - // Future.delayed(const Duration(milliseconds: 500), () async { - // final MapEntry? lastChange = - // _newsMarkQueue.lastWhereOrNull((item) => item.key == id); - // if (lastChange == null) return; - // final service = RequestService(RequestHelper.mark(id, 'news')); - // if (lastChange.value) { - // await service.post(); - // } else { - // await service.delete(); - // } - // _newsMarkQueue.removeWhere((element) => element.key == id); - // }); + //   _newsMarkQueue.add(MapEntry(id, value)); + //   Future.delayed(const Duration(milliseconds: 500), () async { + //     final MapEntry? lastChange = + //         _newsMarkQueue.lastWhereOrNull((item) => item.key == id); + //     if (lastChange == null) return; + //     final service = RequestService(RequestHelper.mark(id, 'news')); + //     if (lastChange.value) { + //       await service.post(); + //     } else { + //       await service.delete(); + //   ---------------------------------------------------------------- + + //   } + //     _newsMarkQueue.removeWhere((element) => element.key == id); + //   }); // } static Future changeStatisticMark(int id, bool value) async { @@ -285,10 +367,10 @@ class UserProvider extends CoreProvier { } // Future getUnreadMessageCount() async { - // final RequestService service = RequestService(RequestHelper.directs); - // await service.httpGet(); - // if (service.isSuccess) { - // _unreadMessageCount = service.result['unread'] ?? 0; - // } + //   final RequestService service = RequestService(RequestHelper.directs); + //   await service.httpGet(); + //   if (service.isSuccess) { + //     _unreadMessageCount = service.result['unread'] ?? 0; + //   } // } -} +} \ No newline at end of file diff --git a/lib/services/media/media.dart b/lib/services/media/media.dart index 9d9ba74..00c0007 100644 --- a/lib/services/media/media.dart +++ b/lib/services/media/media.dart @@ -119,7 +119,7 @@ class MediaService { try { final FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, - allowedExtensions: ['pdf'], + allowedExtensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv'], allowMultiple: false, ); return result; diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index c77be45..7f36291 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -3,6 +3,7 @@ import 'package:didvan/models/requests/news.dart'; import 'package:didvan/models/requests/newstats_general.dart'; import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/models/requests/studio.dart'; +import 'package:get/get_connect/http/src/request/request.dart'; class RequestHelper { static const String baseUrl = 'https://api.didvan.app'; @@ -233,6 +234,7 @@ class RequestHelper { static String deleteAllChats() => '$baseUrl/ai/chat/all'; static String archivedChat(int id) => '$baseUrl/ai/chat/$id/archive'; static String placeholder(int id) => '$baseUrl/ai/chat/$id/placeholder'; + static String aiSummery() => '$baseUrl/ai/aisummery'; static String tools() => '$baseUrl/ai/tool'; static String info() => '$baseUrl/ai/video'; static String usersAssistants({final bool personal = false}) => @@ -278,4 +280,9 @@ class RequestHelper { } return null; } + + static String aiSuggestedQuestions() { + return '$baseUrl/ai/aiquestion'; + } + } diff --git a/lib/views/ai/ai_chat_page.dart b/lib/views/ai/ai_chat_page.dart index d28741c..c1b98ba 100644 --- a/lib/views/ai/ai_chat_page.dart +++ b/lib/views/ai/ai_chat_page.dart @@ -1,18 +1,22 @@ +// lib/views/ai/ai_chat_page.dart // ignore_for_file: library_private_types_in_public_api, deprecated_member_use, depend_on_referenced_packages, unnecessary_import - +import 'package:didvan/views/ai/ai_state.dart'; +import 'package:didvan/views/widgets/hoshan_app_bar.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; import 'package:flutter/foundation.dart' show kIsWeb; - import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/main.dart'; import 'package:didvan/models/ai/ai_chat_args.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/models/ai/ai_model_enum.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/view/alert_data.dart'; +import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; @@ -35,13 +39,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; class AiChatPage extends StatefulWidget { final AiChatArgs args; const AiChatPage({Key? key, required this.args}) : super(key: key); - @override _AiChatPageState createState() => _AiChatPageState(); } @@ -49,58 +53,143 @@ class AiChatPage extends StatefulWidget { class _AiChatPageState extends State { final GlobalKey scaffKey = GlobalKey(); FocusNode focusNode = FocusNode(); + BotsModel? _searchBot; + BotsModel? _geminiBot; + BotsModel? _grokBot; + bool _isSearchMode = false; + AiModel _selectedModel = AiModel.chatGPT; + late BotsModel _currentBot; @override void didUpdateWidget(covariant AiChatPage oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.args.bot.id != oldWidget.args.bot.id) { - final state = context.read(); - state.clearChat(); - - if (widget.args.chat != null) { - state.chatId = widget.args.chat!.id!; - state.chat = widget.args.chat; - state.getAllMessages(state.chatId!); - } - - - if (widget.args.prompts != null) { + super.didUpdateWidget(oldWidget); + if (widget.args.bot.id != oldWidget.args.bot.id) { + debugPrint("--- AI CHAT PAGE: Bot Updated ---"); + debugPrint( + "Bot ID: ${widget.args.bot.id}, Bot Name: ${widget.args.bot.name}"); + debugPrint("---------------------------------"); + setState(() { + _currentBot = widget.args.bot; + _isSearchMode = _currentBot.id == 36; // Reset search mode + }); + final state = context.read(); + state.clearChat(); + if (widget.args.bot.id == 35) { + state.fetchSuggestedQuestions(); + } + if (widget.args.chat != null) { + state.chatId = widget.args.chat!.id!; + state.chat = widget.args.chat; + state.getAllMessages(state.chatId!); + } + if (widget.args.prompts != null) { state.messages.add(MessageModel( dateTime: DateTime.now() .subtract(const Duration(minutes: 210)) .toIso8601String(), prompts: [widget.args.prompts!])); - state.message.clear(); state.update(); - state.postMessage( - widget.args.bot, widget.args.assistantsName != null); + state.postMessage(widget.args.bot, widget.args.assistantsName != null); } + } } -} + @override void initState() { + super.initState(); + debugPrint("--- AI CHAT PAGE: Init ---"); + debugPrint( + "Bot ID: ${widget.args.bot.id}, Bot Name: ${widget.args.bot.name}"); + debugPrint("----------------------------"); + _currentBot = widget.args.bot; + _isSearchMode = _currentBot.id == 36; final state = context.read(); if (widget.args.chat != null) { state.chatId = widget.args.chat!.id!; state.chat = widget.args.chat; } - - WidgetsBinding.instance.addPostFrameCallback((_) async { + try { + final bots = context.read().bots; + if (mounted) { + setState(() { + _searchBot = bots.firstWhere((b) => b.id == 36); + try { + _geminiBot = bots.firstWhere((b) => b.id == 35 && (b.name?.toLowerCase().contains('gemini') ?? false)); + } catch (e) { + _geminiBot = BotsModel( + id: 35, + name: 'Gemini', + responseType: 'text', + attachmentType: ['audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx'], + attachment: 1, + ); + } + try { + _grokBot = bots.firstWhere((b) => b.id == 35 && (b.name?.toLowerCase().contains('grok') ?? false)); + } catch (e) { + _grokBot = BotsModel( + id: 35, + name: 'Grok', + responseType: 'text', + attachmentType: ['audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx'], + attachment: 1, + ); + } + }); + } + } catch (e) { + if (mounted) { + setState(() { + _searchBot = BotsModel( + id: 36, + name: 'GPT-4o-mini-search', + responseType: 'text', + attachmentType: ['audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx'], + attachment: 1, + ); + _geminiBot = BotsModel( + id: 35, + name: 'Gemini', + responseType: 'text', + attachmentType: ['audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx'], + attachment: 1, + ); + _grokBot = BotsModel( + id: 35, + name: 'Grok', + responseType: 'text', + attachmentType: ['audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx'], + attachment: 1, + ); + }); + debugPrint( + "Search bot (ID 36) not found in History state: $e. Using fallback with audio support."); + } + } + if (widget.args.bot.id == 35) { + state.fetchSuggestedQuestions(); + } if (state.chatId != null) { state.getAllMessages(state.chatId!).then((value) => Future.delayed( - const Duration( - milliseconds: 100, - ), - () => focusNode.requestFocus(), + const Duration(milliseconds: 100), + () { + focusNode.requestFocus(); + // اسکرول خودکار فقط در صورت وجود پیام + if (state.messages.isNotEmpty) { + state.scrollController.animateTo( + state.scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }, )); } else { - state.appState = AppState.idle; + state.appState = AppState.idle; Future.delayed( - const Duration( - milliseconds: 100, - ), + const Duration(milliseconds: 100), () => focusNode.requestFocus(), ); } @@ -110,14 +199,32 @@ class _AiChatPageState extends State { .subtract(const Duration(minutes: 210)) .toIso8601String(), prompts: [widget.args.prompts!])); - state.message.clear(); state.update(); await state.postMessage( widget.args.bot, widget.args.assistantsName != null); + // اسکرول خودکار پس از ارسال پیام + Future.delayed(const Duration(milliseconds: 100), () { + state.scrollController.animateTo( + state.scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + }); } }); - super.initState(); + } + + BotsModel _getSelectedBot() { + switch (_selectedModel) { + case AiModel.gemini: + return _geminiBot ?? _currentBot; + case AiModel.grok: + return _grokBot ?? _currentBot; + case AiModel.chatGPT: + default: + return _currentBot; + } } @override @@ -128,152 +235,333 @@ class _AiChatPageState extends State { context.read().getChats(); context.read().refresh = false; } + // اول state را reset می‌کنیم + context.read().endChat(); + + // صبر می‌کنیم تا frame بعدی render شود + await Future.delayed(Duration.zero); + + // حالا اجازه pop می‌دهیم return true; }, child: Consumer( builder: (context, state, child) => Scaffold( - // appBar: HoshanAppBar( - // onBack: () { - // Navigator.pop(context); - // }, - // withActions: false, - // ), - key: scaffKey, - resizeToAvoidBottomInset: true, - drawer: HoshanDrawer( - scaffKey: scaffKey, - ), - body: StateHandler( - state: state, - onRetry: () { - if (state.chatId != null) { - state.getAllMessages(state.chatId!); - } - }, - builder: (context, state) { - return Stack( - children: [ - SingleChildScrollView( - reverse: true, - controller: state.scrollController, - child: Column( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox( - height: 24, - ), - // SkeletonImage( - // width: 75, - // height: 75, - // imageUrl: widget.args.bot.image.toString(), - // borderRadius: BorderRadius.circular(360), - // ), - // const SizedBox( - // height: 12, - // ), - // DidvanText( - // widget.args.assistantsName ?? - // widget.args.bot.name.toString(), - // fontSize: 17, - // fontWeight: FontWeight.bold, - // ), - if (state.messages.isEmpty) - Column( - children: [ - const SizedBox( - height: 16, - ), - Padding( - padding: const EdgeInsets.only(left: 60,right: 60), - child: Center( - child: DidvanText( - widget.args.bot.description ?? '', - fontSize: 12, - color: Theme.of(context) - .colorScheme - .caption, - textAlign: TextAlign.justify, - )), - ), - const SizedBox( - height: 100, - ), - ], - ) - ], - ), - if (state.messages.isNotEmpty) - ListView.builder( - itemCount: state.messages.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.only( - bottom: state.file != null && - !(state.file!.isRecorded) - ? 180 - : 100), - itemBuilder: (context, mIndex) { - final prompts = - state.messages[mIndex].prompts; - final time = - state.messages[mIndex].dateTime; - return Column( - children: [ - timeLabel(context, time), - ListView.builder( - itemCount: prompts.length, - shrinkWrap: true, - physics: - const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - final message = prompts[index]; + appBar: HoshanAppBar( + onBack: () async { + if (context.read().refresh) { + context.read().getChats(); + context.read().refresh = false; + } + // اول state را reset می‌کنیم + context.read().endChat(); - return messageBubble(message, - context, state, index, mIndex); - }, + // صبر می‌کنیم تا state تغییر کند + await Future.delayed(const Duration(milliseconds: 50)); + + // حالا صفحه را می‌بندیم + if (context.mounted) { + Navigator.pop(context); + } + }, + withActions: false, + ), + key: scaffKey, + resizeToAvoidBottomInset: true, + drawer: HoshanDrawer( + scaffKey: scaffKey, + ), + body: StateHandler( + state: state, + onRetry: () { + if (state.chatId != null) { + state.getAllMessages(state.chatId!); + } + if (widget.args.bot.id == 35) { + state.fetchSuggestedQuestions(); + } + }, + builder: (context, state) { + final userProvider = context.watch(); + return SingleChildScrollView( + reverse: true, + controller: state.scrollController, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (state.messages.isEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 90), // کاهش فضای خالی + userProvider.isLoadingWelcome + ? const SizedBox(height: 20) + : userProvider.welcomeMessage != null + ? Padding( + padding: const EdgeInsets.only( + bottom: 12.0, left: 24, right: 24), + child: Center( + child: DidvanText( + userProvider.welcomeMessage!, + textAlign: TextAlign.center, + fontSize: 18, + fontWeight: FontWeight.bold, + color: const Color.fromARGB( + 255, 0, 126, 167), + ), + ), + ) + : const SizedBox(height: 20), + const Text( + 'چطور می‌تونم کمکت کنم؟', + style: TextStyle( + color: Color.fromARGB(255, 0, 53, 70), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 70), + if (widget.args.bot.id == 35) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), // کاهش فضای خالی + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0), + child: Row( + children: [ + const SizedBox(width: 7), + SvgPicture.asset( + 'lib/assets/icons/message-question.svg', + height: 25, + color: const Color.fromARGB( + 255, 0, 126, 167), + ), + const SizedBox(width: 7), + const DidvanText( + 'سوالات پیشنهادی:', + fontSize: 16, + fontWeight: FontWeight.normal, + color: Color.fromARGB(255, 0, 126, 167), ), ], - ); - }), + ), + ), + const SizedBox(height: 12), + state.isLoadingQuestions + ? Center( + child: SpinKitThreeBounce( + color: Theme.of(context) + .colorScheme + .primary, + size: 18, + ), + ) + : state.suggestedQuestions.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0), + child: Center( + child: DidvanText( + 'سوالی برای پیشنهاد وجود ندارد.', + fontSize: 12, + color: Theme.of(context) + .colorScheme + .caption, + ), + ), + ) + : ListView.builder( + shrinkWrap: true, + physics: + const NeverScrollableScrollPhysics(), + itemCount: + state.suggestedQuestions.length, + itemBuilder: (context, index) { + final question = state + .suggestedQuestions[index]; + return InkWell( + onTap: () { + 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: question, + fileLocal: null, + 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: question, + finished: true, + fileLocal: null, + role: 'user', + createdAt: DateTime + .now() + .subtract( + const Duration( + minutes: + 210)) + .toIso8601String(), + ) + ])); + } + state.message.clear(); + state.update(); + state.postMessage( + _currentBot, + widget.args + .assistantsName != + null); + // اسکرول خودکار پس از کلیک روی سوال پیشنهادی + Future.delayed( + const Duration( + milliseconds: 100), + () { + state.scrollController + .animateTo( + state + .scrollController + .position + .maxScrollExtent, + duration: const Duration( + milliseconds: 300), + curve: Curves.easeOut, + ); + }); + }, + child: Column( + children: [ + Container( + margin: const EdgeInsets + .symmetric( + horizontal: 24, + vertical: 4), + child: DidvanText( + question, + fontSize: 12, + color: const Color + .fromARGB( + 255, 102, 102, 102), + ), + ), + const Padding( + padding: + EdgeInsets.fromLTRB( + 110, 5, 20, 5), + child: Divider( + height: 1, + color: Color.fromARGB( + 255, 210, 210, 210), + ), + ), + ], + ), + ); + }, + ), + const SizedBox(height: 10), // کاهش فضای خالی + ], + ) + else + const SizedBox(height: 20), // کاهش فضای خالی ], ), - ), - Positioned( - top: 32, - right: 0, - child: InkWell( - onTap: () => scaffKey.currentState!.openDrawer(), - child: Container( - width: 46, - height: 46, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - bottomLeft: Radius.circular(12)), - boxShadow: DesignConfig.defaultShadow), - child: Icon( - DidvanIcons.angle_left_light, - color: Theme.of(context).colorScheme.title, - ), - )), - ) + if (state.messages.isNotEmpty) + ListView.builder( + itemCount: state.messages.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, mIndex) { + final prompts = state.messages[mIndex].prompts; + final time = state.messages[mIndex].dateTime; + return Column( + children: [ + timeLabel(context, time), + ListView.builder( + itemCount: prompts.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final message = prompts[index]; + return messageBubble( + message, context, state, index, mIndex); + }, + ), + ], + ); + }, + ), ], - ); - }, + ), + ); + }, + ), + bottomNavigationBar: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, ), - bottomSheet: Column( + child: Column( mainAxisSize: MainAxisSize.min, children: [ AiMessageBar( - bot: widget.args.bot, + bot: _isSearchMode && _searchBot != null + ? _searchBot! + : _getSelectedBot(), attch: widget.args.attach, assistantsName: widget.args.assistantsName, + showSearchToggle: + _currentBot.id == 35 && _searchBot != null, + isSearchMode: _isSearchMode, + onSearchModeToggled: (bool isSearchOn) { + if (_searchBot == null) return; + setState(() { + _isSearchMode = isSearchOn; + }); + }, + showModelSelector: + _currentBot.id == 35 && _geminiBot != null && _grokBot != null, + selectedModel: _selectedModel, + onModelChanged: (AiModel model) { + setState(() { + _selectedModel = model; + }); + }, ), ], - )), + ), + ), + ), ), ); } @@ -307,7 +595,6 @@ class _AiChatPageState extends State { duration: message.duration != null ? Duration(seconds: message.duration!) : null)); - return Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), child: Column( @@ -349,31 +636,32 @@ class _AiChatPageState extends State { ? Column( children: [ ValueListenableBuilder>( - valueListenable: state.messageOnstream, - builder: (context, value, child) { - return StreamBuilder( - stream: value, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const SizedBox(); - } - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16), - child: Directionality( - textDirection: snapshot.data - .toString() - .startsWithEnglish() - ? TextDirection.ltr - : TextDirection.rtl, - child: DidvanMarkdownText( - text: "${snapshot.data}...", - ), + valueListenable: state.messageOnstream, + builder: (context, value, child) { + return StreamBuilder( + stream: value, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const SizedBox(); + } + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 16), + child: Directionality( + textDirection: snapshot.data + .toString() + .startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + child: DidvanMarkdownText( + text: "${snapshot.data}...", ), - ); - }, - ); - }), + ), + ); + }, + ); + }, + ), Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -381,7 +669,7 @@ class _AiChatPageState extends State { color: Theme.of(context).colorScheme.primary, size: 18, ), - ) + ), ], ) : Column( @@ -401,13 +689,14 @@ class _AiChatPageState extends State { padding: const EdgeInsets.fromLTRB( 16, 16, 16, 0), child: ClipRRect( - borderRadius: - DesignConfig.lowBorderRadius, - child: ChatVideoPlayer( - src: RequestHelper.baseUrl + - file.path, - custome: const CustomControls(), - )), + borderRadius: + DesignConfig.lowBorderRadius, + child: ChatVideoPlayer( + src: RequestHelper.baseUrl + + file.path, + custome: const CustomControls(), + ), + ), ) : file.isImage() ? Padding( @@ -416,9 +705,8 @@ class _AiChatPageState extends State { child: messageImage(file), ) : Padding( - padding: const EdgeInsets.all( - 8.0, - ), + padding: + const EdgeInsets.all(8.0), child: messageFile( context, message, state), ), @@ -452,104 +740,71 @@ class _AiChatPageState extends State { .contains('user') && widget.args.assistantsName == null) PopupMenuButton( - offset: const Offset(0, 46), - onSelected: (value) async { - navigatorKey.currentState!.pushNamed( - Routes.aiChat, - arguments: AiChatArgs( - bot: value, - prompts: message, - isTool: widget.args.isTool ?? - context - .read< - HistoryAiChatState>() - .bots)); - }, - itemBuilder: (BuildContext context) { - final bots = widget.args.isTool ?? - context - .read() - .bots; - return [ - ...List.generate( - bots.length, - (index) => PopupMenuItem( - value: bots[index], - height: 72, - child: Container( - constraints: - const BoxConstraints( - maxWidth: 200), - child: Row( - children: [ - SkeletonImage( - imageUrl: bots[index] - .image - .toString(), - width: 42, - height: 42, - borderRadius: - BorderRadius - .circular(360), - ), - const SizedBox(width: 12), - Expanded( - child: Directionality( - textDirection: - TextDirection.ltr, - child: DidvanText( - bots[index] - .name - .toString(), - maxLines: 1, - overflow: - TextOverflow - .ellipsis, - ), + offset: const Offset(0, 46), + onSelected: (value) async { + navigatorKey.currentState!.pushNamed( + Routes.aiChat, + arguments: AiChatArgs( + bot: value, + prompts: message, + isTool: widget.args.isTool ?? + context + .read() + .bots, + ), + ); + }, + itemBuilder: (BuildContext context) { + final bots = widget.args.isTool ?? + context + .read() + .bots; + return [ + ...List.generate( + bots.length, + (index) => PopupMenuItem( + value: bots[index], + height: 72, + child: Container( + constraints: + const BoxConstraints( + maxWidth: 200), + child: Row( + children: [ + SkeletonImage( + imageUrl: bots[index] + .image + .toString(), + width: 42, + height: 42, + borderRadius: + BorderRadius.circular( + 360), + ), + const SizedBox(width: 12), + Expanded( + child: Directionality( + textDirection: + TextDirection.ltr, + child: DidvanText( + bots[index] + .name + .toString(), + maxLines: 1, + overflow: TextOverflow + .ellipsis, ), ), - ], - ), + ), + ], ), ), - ) - ]; - }, - child: const SizedBox(), - // Container( - // alignment: Alignment.center, - // margin: const EdgeInsets.all(8), - // padding: const EdgeInsets.symmetric( - // horizontal: 8), - // constraints: const BoxConstraints( - // maxWidth: 100), - // decoration: BoxDecoration( - // borderRadius: - // DesignConfig.lowBorderRadius, - // border: Border.all( - // color: Theme.of(context) - // .colorScheme - // .title)), - // child: Row( - // children: [ - // Expanded( - // child: Directionality( - // textDirection: - // TextDirection.ltr, - // child: DidvanText( - // '${widget.args.assistantsName ?? widget.args.bot.name}', - // maxLines: 1, - // overflow: - // TextOverflow.ellipsis, - // ), - // ), - // ), - // const Icon( - // DidvanIcons.angle_down_light), - // ], - // ), - // ) - ), + ), + ), + ]; + }, + child: const SizedBox(), + ), if (message.role .toString() .contains('user') && @@ -557,8 +812,8 @@ class _AiChatPageState extends State { state.messages[mIndex].prompts .length - 2 && - (widget.args.bot.editable != null && - widget.args.bot.editable!)) + (_currentBot.editable != null && + _currentBot.editable!)) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( @@ -627,10 +882,26 @@ class _AiChatPageState extends State { message.copyWith(error: false)); state.file = file; state.update(); + final botToUse = _isSearchMode && + _searchBot != null + ? _searchBot! + : _getSelectedBot(); await state.postMessage( - widget.args.bot, + botToUse, widget.args.assistantsName != null); + // اسکرول خودکار پس از ارسال مجدد پیام + Future.delayed( + const Duration(milliseconds: 100), + () { + state.scrollController.animateTo( + state.scrollController.position + .maxScrollExtent, + duration: const Duration( + milliseconds: 300), + curve: Curves.easeOut, + ); + }); }, child: Icon( DidvanIcons.refresh_solid, @@ -641,7 +912,9 @@ class _AiChatPageState extends State { ), ), ), - if (message.text != null && message.text!.isNotEmpty && (file == null || !file.isImage())) + if (message.text != null && + message.text!.isNotEmpty && + (file == null || !file.isImage())) Padding( padding: const EdgeInsets.all(8.0), child: InkWell( @@ -653,11 +926,12 @@ class _AiChatPageState extends State { Future.delayed( Duration.zero, () => ActionSheetUtils(context) - .showAlert(AlertData( - message: - "متن با موفقیت کپی شد", - aLertType: - ALertType.success)), + .showAlert( + AlertData( + message: "متن با موفقیت کپی شد", + aLertType: ALertType.success, + ), + ), ); }, child: Icon( @@ -698,7 +972,7 @@ class _AiChatPageState extends State { ], ), ), - ) + ), ], ), const SizedBox(height: 4), @@ -730,9 +1004,7 @@ class _AiChatPageState extends State { child: Row( children: [ const Icon(Icons.file_copy), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -750,7 +1022,7 @@ class _AiChatPageState extends State { ), ], ), - ) + ), ], ), ); @@ -764,7 +1036,8 @@ class _AiChatPageState extends State { ? file.path.startsWith('blob:') ? ClipRRect( borderRadius: DesignConfig.lowBorderRadius, - child: Image.network(file.path)) + child: Image.network(file.path), + ) : SkeletonImage( pWidth: MediaQuery.sizeOf(context).width / 1, pHeight: MediaQuery.sizeOf(context).height / 6, @@ -772,7 +1045,8 @@ class _AiChatPageState extends State { ) : ClipRRect( borderRadius: DesignConfig.lowBorderRadius, - child: Image.file(file.main)), + child: Image.file(file.main), + ), ); } -} \ No newline at end of file +} diff --git a/lib/views/ai/ai_chat_state.dart b/lib/views/ai/ai_chat_state.dart index cc7ed20..f5ba32c 100644 --- a/lib/views/ai/ai_chat_state.dart +++ b/lib/views/ai/ai_chat_state.dart @@ -1,3 +1,5 @@ +// lib/views/ai/ai_chat_state.dart + // ignore_for_file: body_might_complete_normally_catch_error, avoid_print import 'dart:async'; @@ -37,6 +39,11 @@ class AiChatState extends CoreProvier { bool isRecorded = false; TextEditingController message = TextEditingController(); + // --- ADDED --- + List suggestedQuestions = []; + bool isLoadingQuestions = true; + // --- END ADDED --- + Future _scrolledEnd() async { WidgetsBinding.instance.addPostFrameCallback((_) async { await scrollController.animateTo( @@ -73,6 +80,38 @@ class AiChatState extends CoreProvier { return service; } + // --- ADDED --- + Future fetchSuggestedQuestions() async { + isLoadingQuestions = true; + suggestedQuestions.clear(); + update(); + + // 1. RequestHelper فقط URL string را برمی‌گرداند + final service = RequestService( + RequestHelper.aiSuggestedQuestions(), + // 2. بدنه درخواست خالی است و هدر Authorization خودکار اضافه می‌شود + ); + + // 3. متد post() را (به جای httpPost) فراخوانی می‌کنیم + await service.post(); + + if (service.isSuccess) { + // 4. از متد data() برای خواندن نتیجه استفاده می‌کنیم + final List questionsList = service.data('Questions'); + if (questionsList.isNotEmpty) { + suggestedQuestions = questionsList.map((q) => q.toString()).toList(); + } + isLoadingQuestions = false; + update(); + } else { + // 5. از errorMessage برای نمایش خطا استفاده می‌کنیم + print("Error fetching suggested questions: ${service.errorMessage}"); + isLoadingQuestions = false; + update(); + } + } + // --- END ADDED --- + Future getAllMessages(int chatId) async { loading = true; onResponsing = true; @@ -151,16 +190,20 @@ class AiChatState extends CoreProvier { .toIso8601String())); update(); await _scrolledEnd(); - // if (file != null) { - // html.AnchorElement anchorElement = html.AnchorElement(href: file!.path); - // anchorElement.download = '${file!.path}.m4a'; - // anchorElement.click(); - // } + + // تعیین URL بر اساس bot ID + String url; + if (bot.id == 100) { + // برای Aisummery از endpoint خاص استفاده می‌کنیم + url = '/100/aisummery'; + } else { + // برای بقیه bot ها از endpoint عادی استفاده می‌کنیم + url = '${isAssistants ? '/user/${bot.responseType}' : ''}/${bot.id}/${bot.name}' + .toLowerCase(); + } final req = await AiApiService.initial( - url: - '${isAssistants ? '/user/${bot.responseType}' : ''}/${bot.id}/${bot.name}' - .toLowerCase(), + url: url, message: message, chatId: chatId, file: uploadedFile, @@ -290,6 +333,7 @@ class AiChatState extends CoreProvier { message: 'خطا در برقراری ارتباط', aLertType: ALertType.error)); update(); } + Future clearChat() async { messages.clear(); chatId = null; @@ -302,4 +346,4 @@ class AiChatState extends CoreProvier { notifyListeners(); await Future.delayed(const Duration(milliseconds: 10)); } -} +} \ No newline at end of file diff --git a/lib/views/ai/widgets/ai_message_bar.dart b/lib/views/ai/widgets/ai_message_bar.dart index 2d9d701..b57dec9 100644 --- a/lib/views/ai/widgets/ai_message_bar.dart +++ b/lib/views/ai/widgets/ai_message_bar.dart @@ -1,7 +1,10 @@ +// lib/views/ai/widgets/ai_message_bar.dart + // ignore_for_file: library_private_types_in_public_api, avoid_web_libraries_in_flutter, deprecated_member_use import 'dart:async'; import 'package:didvan/utils/extension.dart'; + import 'package:record/record.dart'; import 'package:universal_html/html.dart' as html; import 'dart:io'; @@ -13,6 +16,7 @@ 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/models/ai/ai_model_enum.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/utils/date_time.dart'; @@ -34,6 +38,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_svg/flutter_svg.dart'; // <-- ایمپورت SVG typedef _Fn = void Function(); @@ -41,9 +46,29 @@ class AiMessageBar extends StatefulWidget { final BotsModel bot; final String? assistantsName; final bool? attch; - const AiMessageBar( - {Key? key, required this.bot, this.attch, this.assistantsName}) - : super(key: key); + // --- ADDED --- + final bool showSearchToggle; + final bool isSearchMode; + final ValueChanged? onSearchModeToggled; + final bool showModelSelector; + final AiModel selectedModel; + final ValueChanged? onModelChanged; + // --- END ADDED --- + + const AiMessageBar({ + Key? key, + required this.bot, + this.attch, + this.assistantsName, + // --- ADDED --- + this.showSearchToggle = false, + this.isSearchMode = false, + this.onSearchModeToggled, + this.showModelSelector = false, + this.selectedModel = AiModel.chatGPT, + this.onModelChanged, + // --- END ADDED --- + }) : super(key: key); @override _AiMessageBarState createState() => _AiMessageBarState(); @@ -231,11 +256,14 @@ class _AiMessageBarState extends State { @override Widget build(BuildContext context) { return Consumer(builder: (context, state, child) { + // --- MODIFIED: Theme definition moved here to be accessible by new toggle --- + final theme = Theme.of(context); + return Container( padding: const EdgeInsets.fromLTRB(12, 24, 12, 0).copyWith( top: (state.file != null && !state.file!.isRecorded) ? 0 : 24), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: theme.colorScheme.background, ), child: Column( children: [ @@ -245,40 +273,233 @@ class _AiMessageBarState extends State { children: [ Container( decoration: BoxDecoration( - boxShadow: DesignConfig.defaultShadow, - color: Theme.of(context).colorScheme.surface, + color: const Color.fromARGB(255, 245, 245, 245), border: Border.all( color: const Color.fromARGB(255, 0, 126, 167), width: 1.5), - borderRadius: BorderRadius.circular(50), + borderRadius: BorderRadius.circular(16), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, + // --- MODIFIED: Changed Row to Column --- + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - recorderAndSendButton(state), - if (!(_mRecorder!.isStopped)) - ValueListenableBuilder( - valueListenable: _countTimer, - builder: (context, value, child) => Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), - child: SizedBox( - width: 50, - child: Center( - child: DidvanText( - DateTimeUtils.normalizeTimeDuration(value)), + // --- MODIFIED: Added new Web Search Toggle --- + + // --- MODIFIED: This is the original Row, now inside the Column --- + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + recorderAndSendButton(state), + if (!(_mRecorder!.isStopped)) + ValueListenableBuilder( + valueListenable: _countTimer, + builder: (context, value, child) => Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + child: SizedBox( + width: 50, + child: Center( + child: DidvanText( + DateTimeUtils.normalizeTimeDuration( + value)), + ), + ), ), ), + Expanded( + child: Padding( + padding: _mRecorder!.isPaused || + _mRecorder!.isRecording + ? const EdgeInsets.fromLTRB(12, 8, 0, 8) + // --- MODIFIED: Adjusted padding when search toggle or model selector is visible --- + : (widget.showSearchToggle || + widget.showModelSelector) + ? EdgeInsets.zero.copyWith(top: 4.0) + : EdgeInsets.zero, + child: + recorderAndTextMessageHandler(context, state), + ), + ), + ], + ), + if (widget.showSearchToggle || widget.showModelSelector) + Padding( + padding: const EdgeInsets.only( + top: 4.0, left: 12.0, right: 12.0, bottom: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Model Selector Dropdown (3 options) + if (widget.showModelSelector) + Builder( + builder: (context) { + String icon; + String label; + + switch (widget.selectedModel) { + case AiModel.gemini: + icon = ''; + label = 'Gemini'; + break; + case AiModel.grok: + icon = 'assets/icons/grok.svg'; + label = 'Grok'; + break; + case AiModel.chatGPT: + default: + icon = 'assets/icons/chat_gpt.svg'; + label = 'ChatGPT'; + } + + return PopupMenuButton( + onSelected: (AiModel model) { + widget.onModelChanged?.call(model); + }, + offset: const Offset(0, 40), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12.0), + ), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: AiModel.chatGPT, + child: Row( + children: [ + Icon( + Icons.chat_bubble_outline, + size: 20, + ), + const SizedBox(width: 12.0), + const DidvanText( + 'ChatGPT', + fontSize: 14, + ), + ], + ), + ), + const PopupMenuItem( + value: AiModel.gemini, + child: Row( + children: [ + Icon( + Icons.auto_awesome, + size: 20, + ), + SizedBox(width: 12.0), + DidvanText( + 'Gemini', + fontSize: 14, + ), + ], + ), + ), + const PopupMenuItem( + value: AiModel.grok, + child: Row( + children: [ + Icon( + Icons.bolt, + size: 20, + ), + SizedBox(width: 12.0), + DidvanText( + 'Grok', + fontSize: 14, + ), + ], + ), + ), + ], + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 6.0), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(100.0), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + icon, + height: 20, + ), + const SizedBox(width: 8.0), + DidvanText( + label, + fontSize: 14, + color: const Color.fromARGB( + 255, 61, 61, 61), + ), + const SizedBox(width: 4.0), + const Icon( + Icons.arrow_drop_down, + size: 20, + color: Color.fromARGB( + 255, 61, 61, 61), + ), + ], + ), + ), + ); + }, + ), + + // Spacer between toggles + if (widget.showSearchToggle && + widget.showModelSelector) + const SizedBox(width: 8.0), + + // Search Toggle + if (widget.showSearchToggle) + Builder( + builder: (context) { + final Color activeColor = + const Color.fromARGB(255, 0, 126, 167); + final bool isSearchMode = + widget.isSearchMode; + final Color backgroundColor = isSearchMode + ? activeColor.withOpacity(0.1) + : theme.colorScheme.surface; + + return InkWell( + onTap: () { + widget.onSearchModeToggled + ?.call(!widget.isSearchMode); + }, + borderRadius: + BorderRadius.circular(100.0), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 6.0), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: + BorderRadius.circular(100.0), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + 'lib/assets/icons/global-search 2.svg', + height: 25, + ), + const SizedBox(width: 8.0), + const DidvanText( + 'جستجو در وب', + fontSize: 14, + color: Color.fromARGB( + 255, 61, 61, 61), + ), + ], + ), + ), + ); + }, + ), + ], ), ), - Expanded( - child: Padding( - padding: - _mRecorder!.isPaused || _mRecorder!.isRecording - ? const EdgeInsets.fromLTRB(12, 8, 0, 8) - : EdgeInsets.zero, - child: recorderAndTextMessageHandler(context, state), - ), - ), ], ), ), @@ -286,23 +507,24 @@ class _AiMessageBarState extends State { Positioned.fill( child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .focused - .withOpacity(0.5)), + color: theme.colorScheme.focused.withOpacity(0.5)), ), ) ], ), + + // --- MODIFIED: Search Toggle UI removed from here --- + // The original Checkbox block that was here has been deleted. + MediaQuery.of(context).viewInsets.bottom == 0 - ? const Padding( + ? const Padding( padding: EdgeInsets.fromLTRB(3, 8, 3, 4), - child: SizedBox(height: 12,) - // DidvanText( - // 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید.', - // fontSize: 11, - // fontWeight: FontWeight.bold, - // ), + child: DidvanText( + 'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید و از وارد کردن اطلاعات حساس بپرهیزید.', + fontSize: 10, + fontWeight: FontWeight.normal, + color: Color.fromARGB(255, 95, 95, 95), + ), ) : const SizedBox( height: 12, @@ -407,7 +629,7 @@ class _AiMessageBarState extends State { Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), child: MessageBarBtn( - enable: false, + enable: true, // <-- فعال کردن دکمه icon: openAttach || state.file != null ? DidvanIcons.close_regular : Icons.add, @@ -441,13 +663,14 @@ class _AiMessageBarState extends State { enabled: !(state.file != null && widget.bot.attachment == 1), decoration: InputDecoration( - contentPadding: const EdgeInsets.fromLTRB(12, 12, 12, 12), + contentPadding: const EdgeInsets.fromLTRB(12, 12, 0, 15), border: InputBorder.none, + // --- MODIFIED: Hint Text is now always the same --- hintText: 'بنویسید...', hintStyle: Theme.of(context) .textTheme .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.disabledText), + .copyWith(color: const Color.fromARGB(255, 95, 95, 95)), suffixIcon: state.isEdite ? InkWell( onTap: () { @@ -472,16 +695,30 @@ class _AiMessageBarState extends State { widget.bot.attachment != 0 ? MessageBarBtn( enable: true, - icon: _mRecorder!.isRecording || _mRecorder!.isPaused - ? Icons.stop_rounded - : DidvanIcons.mic_regular, + iconWidget: _mRecorder!.isRecording || _mRecorder!.isPaused + ? SvgPicture.asset( + 'lib/assets/icons/microphone-3.svg', + width: 24, + height: 24, + ) + : SvgPicture.asset( + 'lib/assets/icons/microphone-3.svg', + width: 29, + height: 29, + ), click: getRecorderFn(), ) + // --- MODIFIED BLOCK: استفاده از iconWidget برای SVG --- : MessageBarBtn( enable: (state.file != null && state.file!.isRecorded) || (widget.bot.attachment == 1) || message.text.isNotEmpty, - icon: DidvanIcons.send_light, + iconWidget: SvgPicture.asset( + 'lib/assets/icons/send3.svg', // <-- مسیر SVG شما + width: 24, // اندازه دلخواه + height: 24, + color: const Color.fromARGB(255, 0, 126, 167), + ), click: () async { if ((state.file == null || !state.file!.isRecorded) && (widget.bot.attachment != 1) && @@ -531,10 +768,12 @@ class _AiMessageBarState extends State { state.message.clear(); openAttach = false; state.update(); + // --- MODIFIED: postMessage uses widget.bot, which is now stateful (normal or search) await state.postMessage( widget.bot, widget.assistantsName != null); }, ), + // --- END MODIFIED BLOCK --- ), ); } @@ -560,8 +799,7 @@ class _AiMessageBarState extends State { String? name = result.files.single.name; if (kIsWeb) { - Uint8List? bytes = result - .files.first.bytes; + Uint8List? bytes = result.files.first.bytes; state.file = FilesModel( '', @@ -691,6 +929,7 @@ class _AiMessageBarState extends State { )); } + // --- MODIFIED BLOCK: کد صحیح تابع --- Widget audioContainer() { final state = context.watch(); @@ -698,11 +937,12 @@ class _AiMessageBarState extends State { width: MediaQuery.sizeOf(context).width, child: AudioWave( file: state.file!.path, - loadingPaddingSize: 8.0, + loadingPaddingSize: 8.0, // <-- این پارامتر درست است totalDuration: _countTimer.value, ), ); } + // --- END MODIFIED BLOCK --- Widget fileContainer() { final state = context.watch(); diff --git a/lib/views/ai/widgets/audio_wave.dart b/lib/views/ai/widgets/audio_wave.dart index ad050f3..4adbe7f 100644 --- a/lib/views/ai/widgets/audio_wave.dart +++ b/lib/views/ai/widgets/audio_wave.dart @@ -98,7 +98,6 @@ class _AudioWaveState extends State { click: () async { await VoiceService.resetVoicePlayer(); }, - loading: true, ); } return MessageBarBtn( diff --git a/lib/views/ai/widgets/message_bar_btn.dart b/lib/views/ai/widgets/message_bar_btn.dart index 02c4ed2..595ceaa 100644 --- a/lib/views/ai/widgets/message_bar_btn.dart +++ b/lib/views/ai/widgets/message_bar_btn.dart @@ -1,54 +1,55 @@ import 'package:didvan/config/theme_data.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_spinkit/flutter_spinkit.dart'; class MessageBarBtn extends StatelessWidget { final bool enable; - final IconData icon; + final IconData? icon; + final Widget? iconWidget; // برای پذیرش ویجت‌های سفارشی final Function()? click; - final Color? color; - final bool loading; - const MessageBarBtn( - {Key? key, - required this.enable, - required this.icon, - this.click, - this.color, - this.loading = false}) - : super(key: key); + + const MessageBarBtn({ + Key? key, + this.icon, + this.iconWidget, + this.click, + required this.enable, + }) : assert(icon != null || iconWidget != null, + 'Either icon or iconWidget must be provided'), // اطمینان از اینکه یکی از آیکن‌ها ارائه شده + super(key: key); @override Widget build(BuildContext context) { - return Container( - width: 32, - height: 32, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: enable - ? color ?? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.border), - child: InkWell( - onTap: click, - child: Stack( - children: [ - Positioned.fill( - child: Icon( - icon, - size: 18, - color: enable ? Theme.of(context).colorScheme.white : null, - ), - ), - if (loading) - const Positioned.fill( - child: Padding( - padding: EdgeInsets.all(8.0), - child: SpinKitCircle( - color: Colors.white, - ), - )) - ], - ), + // --- BLOCK MODIFIED --- + // ویجت آیکن نهایی را مشخص می‌کنیم + Widget finalIconWidget; + + if (iconWidget != null) { + // اگر ویجت سفارشی (SVG) داریم، رنگ اصلی‌اش را حفظ می‌کنیم + // و فقط برای حالت غیرفعال، آن را کم‌رنگ می‌کنیم + finalIconWidget = Opacity( + opacity: enable ? 1.0 : 0.5, // شفافیت بر اساس فعال بودن + child: iconWidget, + ); + } else { + // اگر آیکن معمولی (IconData) داریم، از رنگ تم استفاده می‌کنیم + final color = enable + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.disabledText; + + finalIconWidget = Icon( + icon, + size: 24, + color: color, // رنگ بر اساس فعال/غیرفعال بودن + ); + } + // --- END MODIFIED BLOCK --- + + return InkWell( + onTap: click, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: finalIconWidget, // نمایش ویجت آیکن نهایی ), ); } -} +} \ No newline at end of file diff --git a/lib/views/ai_section/ai_section_page.dart b/lib/views/ai_section/ai_section_page.dart index 6866aed..5bbbea1 100644 --- a/lib/views/ai_section/ai_section_page.dart +++ b/lib/views/ai_section/ai_section_page.dart @@ -1,7 +1,6 @@ -import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/models/ai/ai_chat_args.dart'; -import 'package:didvan/views/ai/ai.dart'; +import 'package:didvan/models/ai/bots_model.dart'; import 'package:didvan/views/ai/ai_chat_page.dart'; import 'package:didvan/views/ai/ai_chat_state.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; @@ -22,11 +21,13 @@ class AiSectionPage extends StatefulWidget { class _AiSectionGridItem { final String title; + final String description; // فیلد جدید اضافه شد final String iconPath; final void Function(BuildContext context) onTap; _AiSectionGridItem({ required this.title, + required this.description, // به سازنده اضافه شد required this.iconPath, required this.onTap, }); @@ -59,8 +60,9 @@ class _AiSectionPageState extends State { _gridItems = [ _AiSectionGridItem( - title: 'تصویرساز', - iconPath: 'lib/assets/icons/houshanNav/imagegeneratorU.svg', + title: 'ساخت عکس', + description: 'ایجاد تصاویر خلاقانه با هوش مصنوعی', // توضیحات اضافه شد + iconPath: 'lib/assets/icons/create image.svg', onTap: (context) { final aiState = context.read(); aiState.endChat(); @@ -76,55 +78,10 @@ class _AiSectionPageState extends State { } }, ), - _AiSectionGridItem( - title: 'چت صوتی', - iconPath: 'lib/assets/icons/houshanNav/aichatU.svg', - onTap: (context) { - final aiState = context.read(); - aiState.endChat(); - if (aiState.tools != null && aiState.tools!.isNotEmpty) { - try { - final tool = aiState.tools!.last; - if (tool.bots != null && tool.bots!.isNotEmpty) { - aiState.startChat(AiChatArgs(bot: tool.bots!.first)); - } - } catch (e) { - debugPrint('خطا در یافتن ابزار چت صوتی: $e'); - } - } - }, - ), - _AiSectionGridItem( - title: 'گفت‌وگو', - iconPath: - 'lib/assets/icons/houshanNav/streamline-flex_ai-scanner-robot.svg', - onTap: (context) { - context.read().endChat(); - Navigator.push( - context, MaterialPageRoute(builder: (context) => const Ai())); - }, - ), - _AiSectionGridItem( - title: 'جست‌وجو', - iconPath: 'lib/assets/icons/houshanNav/searchU.svg', - onTap: (context) { - final aiState = context.read(); - aiState.endChat(); - if (aiState.tools != null && aiState.tools!.length > 1) { - try { - final tool = aiState.tools![1]; - if (tool.bots != null && tool.bots!.isNotEmpty) { - aiState.startChat(AiChatArgs(bot: tool.bots!.first)); - } - } catch (e) { - debugPrint('خطا در یافتن ابزار جست‌وجو: $e'); - } - } - }, - ), _AiSectionGridItem( title: 'ترجمه', - iconPath: 'lib/assets/icons/houshanNav/translateU.svg', + description: 'ترجمه متون به زبان‌های مختلف', // توضیحات اضافه شد + iconPath: 'lib/assets/icons/translate.svg', onTap: (context) { final aiState = context.read(); aiState.endChat(); @@ -140,6 +97,26 @@ class _AiSectionPageState extends State { } }, ), + _AiSectionGridItem( + title: 'خلاصه‌ساز', + description: 'ساخت نمودار با تحلیل هوشمند', + iconPath: 'lib/assets/icons/summary.svg', + onTap: (context) { + final aiState = context.read(); + aiState.endChat(); + + // ساخت BotsModel برای Aisummery + final aisummeryBot = BotsModel( + id: 100, + name: 'Aisummery', + responseType: 'text', + attachmentType: ['pdf', 'image', 'audio'], + attachment: 1, + ); + + aiState.startChat(AiChatArgs(bot: aisummeryBot)); + }, + ), ]; } @@ -154,7 +131,32 @@ class _AiSectionPageState extends State { Expanded( child: aiState.isChatting ? AiChatPage(args: aiState.currentChatArgs!) - : _buildAiGrid(context, aiState), + : SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + SvgPicture.asset('lib/assets/icons/clarity_tools-line.svg'), + const SizedBox(width: 8,), + DidvanText( + 'جعبه ابزار استراتژیک هوشان', + style: Theme.of(context).textTheme.titleMedium, + color: Theme.of(context).colorScheme.title, + ), + ], + ), + ), + SizedBox( + height: MediaQuery.of(context).size.height - 200, + child: _buildAiGrid(context, aiState), + ), + ], + ), + ), ), ], ); @@ -169,18 +171,42 @@ class _AiSectionPageState extends State { child: DidvanText('لیست ابزارها هنوز بارگذاری نشده یا خالی است.')); } - return GridView.builder( - padding: const EdgeInsets.all(16.0), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16.0, - mainAxisSpacing: 16.0, - childAspectRatio: 3 / 2.2, - ), - itemCount: _gridItems.length, - itemBuilder: (context, index) { - final item = _gridItems[index]; - return _buildGridItemCard(context, item); + return ListView.builder( + scrollDirection: Axis.horizontal, + reverse: true, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + itemCount: (_gridItems.length / 2).ceil(), + itemBuilder: (context, columnIndex) { + return Padding( + padding: EdgeInsets.only( + left: columnIndex == (_gridItems.length / 2).ceil() - 1 ? 0 : 16.0, + right: 16.0, + ), + child: SizedBox( + width: 180, + height: 340, // ارتفاع افزایش یافت (قبلا 300) + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (columnIndex * 2 < _gridItems.length) + SizedBox( + width: 180, + height: 160, // ارتفاع کارت افزایش یافت (قبلا 140) + child: _buildGridItemCard(context, _gridItems[columnIndex * 2]), + ), + if (columnIndex * 2 + 1 < _gridItems.length) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: SizedBox( + width: 180, + height: 160, // ارتفاع کارت افزایش یافت (قبلا 140) + child: _buildGridItemCard(context, _gridItems[columnIndex * 2 + 1]), + ), + ), + ], + ), + ), + ); }, ); } @@ -188,35 +214,45 @@ class _AiSectionPageState extends State { Widget _buildGridItemCard(BuildContext context, _AiSectionGridItem item) { return InkWell( onTap: () => item.onTap(context), - borderRadius: DesignConfig.mediumBorderRadius, + borderRadius: BorderRadius.circular(25), child: Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: DesignConfig.mediumBorderRadius, - boxShadow: DesignConfig.defaultShadow, + color: const Color.fromARGB(255, 245, 245, 245), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: const Color.fromARGB(255, 184, 184, 184), + width: 1, + ), ), padding: const EdgeInsets.all(12.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ SvgPicture.asset( item.iconPath, width: 48, height: 48, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.title, - BlendMode.srcIn, - ), ), - const SizedBox(height: 12), + const SizedBox(height: 8), DidvanText( item.title, style: Theme.of(context).textTheme.titleSmall, - color: Theme.of(context).colorScheme.title, + color: const Color.fromARGB(255, 0, 126, 167), maxLines: 1, overflow: TextOverflow.ellipsis, ), + const SizedBox(height: 4), // فاصله برای توضیحات + Expanded( // برای مدیریت بهتر فضا و جلوگیری از overflow + child: DidvanText( + item.description, // نمایش توضیحات + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context).colorScheme.caption, + ), + maxLines: 2, // حداکثر 2 خط + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), diff --git a/lib/views/direct/widgets/message_box.dart b/lib/views/direct/widgets/message_box.dart index 3705908..8d50579 100644 --- a/lib/views/direct/widgets/message_box.dart +++ b/lib/views/direct/widgets/message_box.dart @@ -249,7 +249,6 @@ class _RecordChecking extends StatelessWidget { MessageBarBtn( enable: true, icon: DidvanIcons.trash_solid, - color: Theme.of(context).colorScheme.error, click: () => state.deleteRecordedFile()), const SizedBox( width: 12, diff --git a/lib/views/widgets/hoshan_app_bar.dart b/lib/views/widgets/hoshan_app_bar.dart index 6b1e2f7..5c33b60 100644 --- a/lib/views/widgets/hoshan_app_bar.dart +++ b/lib/views/widgets/hoshan_app_bar.dart @@ -1,13 +1,19 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/widgets/didvan/icon_button.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/hoshan_home_app_bar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:provider/provider.dart'; class HoshanAppBar extends StatelessWidget implements PreferredSizeWidget { final Function()? onBack; final bool withInfo; final bool withActions; + const HoshanAppBar( {Key? key, this.onBack, this.withActions = true, this.withInfo = true}) : super(key: key); @@ -15,37 +21,23 @@ class HoshanAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20)), - color: Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: const Color(0XFF1B3C59).withValues(alpha: 0.15), - blurRadius: 8, - spreadRadius: 0, - offset: const Offset(0, 8), - ) - ], - ), padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - Icon( - DidvanIcons.ai_solid, - size: 40, - color: Theme.of(context).colorScheme.title, - ), - DidvanText( - 'هوشان', - fontSize: 14, - color: Theme.of(context).colorScheme.title, - fontWeight: FontWeight.bold, - ), + SvgPicture.asset( + Assets.horizontalLogoWithText, + height: 60, + width: 60, + ) + // DidvanText( + // 'هوشان', + // fontSize: 14, + // color: Theme.of(context).colorScheme.title, + // fontWeight: FontWeight.bold, + // ), ], ), Row( @@ -58,37 +50,80 @@ class HoshanAppBar extends StatelessWidget implements PreferredSizeWidget { // Navigator.pushNamed(context, Routes.info); // }), // if (withActions) - // Stack( - // children: [ - // DidvanIconButton( - // icon: DidvanIcons.ai_regular, - // size: 32, - // onPressed: () { - // context.read().getMyAssissmant(); + // Stack( + // children: [ + // DidvanIconButton( + // icon: DidvanIcons.ai_regular, + // size: 32, + // onPressed: () { + // context.read().getMyAssissmant(); - // Navigator.pushNamed(context, Routes.botAssistants); - // }, - // ), - // Icon( - // CupertinoIcons.plus, - // color: Theme.of(context).colorScheme.primary, - // size: 16, - // ) - // ], - // ), + // Navigator.pushNamed(context, Routes.botAssistants); + // }, + // ), + // Icon( + // CupertinoIcons.plus, + // color: Theme.of(context).colorScheme.primary, + // size: 16, + // ) + // ], + // ), + + GestureDetector( + onTap: () { + print('history bottom tapped'); + final historyState = context.read(); + if (historyState.chats.isEmpty) { + historyState.getChats(); + } + showHistoryDrawer(context); + }, + child: Container( + padding: const EdgeInsets.all(8), + child: SvgPicture.asset( + 'lib/assets/icons/history.svg', + height: 24, + ), + ), + ), + const SizedBox(width: 5,), if (withInfo) DidvanIconButton( icon: DidvanIcons.angle_left_light, size: 32, onPressed: () => onBack?.call(), ), - ], ) ], )); } + void showHistoryDrawer(BuildContext context) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + barrierColor: Colors.black54, + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (context, animation, secondaryAnimation) { + return const HistoryDrawerContent(); + }, + transitionBuilder: (context, animation, secondaryAnimation, child) { + return SlideTransition( + position: Tween( + begin: const Offset(-1, 0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, + )), + child: child, + ); + }, + ); + } + @override Size get preferredSize => const Size(double.infinity, 144); -} \ No newline at end of file +} diff --git a/lib/views/widgets/hoshan_home_app_bar.dart b/lib/views/widgets/hoshan_home_app_bar.dart index 44facd4..171c01d 100644 --- a/lib/views/widgets/hoshan_home_app_bar.dart +++ b/lib/views/widgets/hoshan_home_app_bar.dart @@ -1,6 +1,7 @@ import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/ai/ai_chat_args.dart'; +import 'package:didvan/providers/user.dart'; // --- ADDED --- import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; @@ -8,7 +9,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:didvan/routes/routes.dart'; import 'package:shamsi_date/shamsi_date.dart'; -import 'package:didvan/services/network/request.dart'; +// import 'package:didvan/services/network/request.dart'; // --- REMOVED --- (دیگه نیازی نیست) class HoshanHomeAppBar extends StatefulWidget { const HoshanHomeAppBar({super.key}); @@ -18,58 +19,20 @@ class HoshanHomeAppBar extends StatefulWidget { } class _HoshanHomeAppBarState extends State { - String? _welcomeMessage; - bool _isLoadingWelcome = true; + // --- REMOVED --- + // String? _welcomeMessage; + // bool _isLoadingWelcome = true; - @override - void initState() { - super.initState(); - _fetchWelcomeMessage(); - } + // @override + // void initState() { + // super.initState(); + // fetchWelcomeMessage(); + // } - Future _fetchWelcomeMessage() async { - try { - const String url = 'https://api.didvan.app/ai/aiwellcom'; + // Future fetchWelcomeMessage() async { ... } + // --- END REMOVED --- - final service = RequestService(url, useAutherization: true); - - await service.post(); - - if (service.isSuccess) { - final period = service.data('period'); - final user = service.data('user'); - if (period != null && user is Map && user.containsKey('fullName')) { - final fullName = user['fullName']; - if (mounted) { - setState(() { - _welcomeMessage = '$period بخیر $fullName 👋'; - _isLoadingWelcome = false; - }); - } - } else { - if (mounted) { - setState(() { - _isLoadingWelcome = false; - }); - } - } - } else { - if (mounted) { - setState(() { - _isLoadingWelcome = false; - }); - } - } - } catch (e) { - if (mounted) { - setState(() { - _isLoadingWelcome = false; - }); - } - } - } - - void _showHistoryDrawer(BuildContext context) { + void showHistoryDrawer(BuildContext context) { showGeneralDialog( context: context, barrierDismissible: true, @@ -77,7 +40,7 @@ class _HoshanHomeAppBarState extends State { barrierColor: Colors.black54, transitionDuration: const Duration(milliseconds: 300), pageBuilder: (context, animation, secondaryAnimation) { - return const _HistoryDrawerContent(); + return const HistoryDrawerContent(); }, transitionBuilder: (context, animation, secondaryAnimation, child) { return SlideTransition( @@ -119,6 +82,10 @@ class _HoshanHomeAppBarState extends State { @override Widget build(BuildContext context) { + // --- ADDED --- + final userProvider = context.watch(); + // --- END ADDED --- + return Container( decoration: const BoxDecoration( color: Color.fromRGBO(230, 242, 246, 1), @@ -150,7 +117,7 @@ class _HoshanHomeAppBarState extends State { if (historyState.chats.isEmpty) { historyState.getChats(); } - _showHistoryDrawer(context); + showHistoryDrawer(context); }, child: Container( padding: const EdgeInsets.all(8), @@ -165,16 +132,18 @@ class _HoshanHomeAppBarState extends State { Container( padding: const EdgeInsets.fromLTRB(8, 35, 8, 35), alignment: Alignment.center, - child: _isLoadingWelcome + // --- MODIFIED --- + child: userProvider.isLoadingWelcome ? const SizedBox(height: 20) - : _welcomeMessage != null + : userProvider.welcomeMessage != null ? DidvanText( - _welcomeMessage!, + userProvider.welcomeMessage!, fontSize: 21, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.title, ) : const SizedBox(height: 20), + // --- END MODIFIED --- ), GestureDetector( onTap: () { @@ -262,14 +231,14 @@ class _HoshanHomeAppBarState extends State { } } -class _HistoryDrawerContent extends StatefulWidget { - const _HistoryDrawerContent(); +class HistoryDrawerContent extends StatefulWidget { + const HistoryDrawerContent(); @override - State<_HistoryDrawerContent> createState() => _HistoryDrawerContentState(); + State createState() => _HistoryDrawerContentState(); } -class _HistoryDrawerContentState extends State<_HistoryDrawerContent> { +class _HistoryDrawerContentState extends State { final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; @@ -796,4 +765,4 @@ class _HistoryDrawerContentState extends State<_HistoryDrawerContent> { ), ); } -} +} \ No newline at end of file