diff --git a/lib/assets/icons/ChatGPT-Logo.svg b/lib/assets/icons/ChatGPT-Logo.svg new file mode 100644 index 0000000..4005d2d --- /dev/null +++ b/lib/assets/icons/ChatGPT-Logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/lib/assets/icons/Google-gemini-icon.svg b/lib/assets/icons/Google-gemini-icon.svg new file mode 100644 index 0000000..787c837 --- /dev/null +++ b/lib/assets/icons/Google-gemini-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/assets/icons/chart-analysis.svg b/lib/assets/icons/chart-analysis.svg new file mode 100644 index 0000000..0d5e4d8 --- /dev/null +++ b/lib/assets/icons/chart-analysis.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/eva_arrow-down-fill.svg b/lib/assets/icons/eva_arrow-down-fill.svg new file mode 100644 index 0000000..ffb41b4 --- /dev/null +++ b/lib/assets/icons/eva_arrow-down-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/explore select.svg b/lib/assets/icons/explore select.svg new file mode 100644 index 0000000..795e598 --- /dev/null +++ b/lib/assets/icons/explore select.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/grok.svg b/lib/assets/icons/grok.svg new file mode 100644 index 0000000..efb1a61 --- /dev/null +++ b/lib/assets/icons/grok.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/lib/assets/icons/text to voice.svg b/lib/assets/icons/text to voice.svg new file mode 100644 index 0000000..535739d --- /dev/null +++ b/lib/assets/icons/text to voice.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/main.dart b/lib/main.dart index c8eeca9..c61c2ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -178,10 +178,7 @@ 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()), diff --git a/lib/models/ai/ai_model_enum.dart b/lib/models/ai/ai_model_enum.dart index 1155a39..c079fd5 100644 --- a/lib/models/ai/ai_model_enum.dart +++ b/lib/models/ai/ai_model_enum.dart @@ -1,5 +1,3 @@ -// lib/models/ai/ai_model_enum.dart - enum AiModel { chatGPT, gemini, diff --git a/lib/providers/user.dart b/lib/providers/user.dart index 3e476d8..9df68ad 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -16,13 +16,11 @@ 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) { @@ -40,54 +38,55 @@ class UserProvider extends CoreProvier { static final List _statisticMarkQueue = []; static final List _itemMarkQueue = []; - // --- ADDED --- Future fetchWelcomeMessage() async { if (!_isLoadingWelcome) { _isLoadingWelcome = true; - notifyListeners(); // برای نمایش لودینگ اگر قبلا مخفی شده بود + notifyListeners(); } try { const String url = 'https://api.didvan.app/ai/aiwellcom'; - // اطمینان از اینکه توکن وجود دارد قبل از ارسال درخواست if (RequestService.token == null) { - print("UserProvider: fetchWelcomeMessage skipped, token is null."); - _isLoadingWelcome = false; // توکن نیست، لودینگ تمام + print("UserProvider: fetchWelcomeMessage skipped, token is null."); + _isLoadingWelcome = false; notifyListeners(); return; } - print("UserProvider: Fetching welcome message..."); // Log start + print("UserProvider: Fetching welcome message..."); final service = RequestService(url, useAutherization: true); await service.post(); if (service.isSuccess) { - print("UserProvider: Welcome message API success."); // Log success + print("UserProvider: Welcome message API 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 userData = service.data('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 + print("UserProvider: Welcome message set: $_welcomeMessage"); } else { - print("UserProvider: Welcome message API success but data format unexpected."); - _welcomeMessage = null; // Clear if data format is wrong + print( + "UserProvider: Welcome message API success but data format unexpected."); + _welcomeMessage = null; } } else { - print("UserProvider: Welcome message API failed. Status: ${service.statusCode}, Error: ${service.errorMessage}"); // Log failure - _welcomeMessage = null; // Clear on API error + print( + "UserProvider: Welcome message API failed. Status: ${service.statusCode}, Error: ${service.errorMessage}"); // Log failure + _welcomeMessage = null; } } catch (e) { - print("UserProvider: Exception fetching welcome message: $e"); // Log exception - _welcomeMessage = null; // Clear on exception + print("UserProvider: Exception fetching welcome message: $e"); + _welcomeMessage = null; } finally { - // همیشه لودینگ رو false کن و UI رو آپدیت کن _isLoadingWelcome = false; - print("UserProvider: fetchWelcomeMessage finished. isLoadingWelcome: $_isLoadingWelcome"); // Log end + print( + "UserProvider: fetchWelcomeMessage finished. isLoadingWelcome: $_isLoadingWelcome"); notifyListeners(); } } - // --- END ADDED --- Future setAndGetToken({String? newToken}) async { try { @@ -103,25 +102,25 @@ class UserProvider extends CoreProvier { } Future getUserInfo() async { - isAuthenticated = true; // Assume authenticated until proven otherwise - print("UserProvider: Getting user info..."); // Log start + isAuthenticated = true; + print("UserProvider: Getting user info..."); final RequestService service = RequestService(RequestHelper.userInfo); await service.httpGet(); if (service.statusCode == 401) { - print("UserProvider: getUserInfo failed - Unauthorized (401)."); - isAuthenticated = false; // Not authenticated + print("UserProvider: getUserInfo failed - Unauthorized (401)."); + isAuthenticated = false; 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; + print("UserProvider: getUserInfo success but user data is null."); + isAuthenticated = false; + return false; } try { - print("UserProvider: User info fetched successfully."); // Log success + print("UserProvider: User info fetched successfully."); user = User.fromJson(service.result['user']); await StorageService.setValue( key: 'notificationTimeRangeStart', @@ -132,30 +131,24 @@ 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 + print("UserProvider: Exception processing user info: $e"); + isAuthenticated = false; return false; } } - // 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; + print( + "UserProvider: getUserInfo failed. Status: ${service.statusCode}, Error: ${service.errorMessage}"); + isAuthenticated = false; + return false; } Future _registerFirebaseToken() async { @@ -373,4 +366,4 @@ class UserProvider extends CoreProvier { //     _unreadMessageCount = service.result['unread'] ?? 0; //   } // } -} \ No newline at end of file +} diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index 7f36291..f9def33 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -3,7 +3,6 @@ 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'; @@ -235,6 +234,8 @@ class RequestHelper { 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 aiAudio() => '$baseUrl/ai/101/aiaudio'; + static String chartAnalysis() => '$baseUrl/ai/27/chart-analysis'; static String tools() => '$baseUrl/ai/tool'; static String info() => '$baseUrl/ai/video'; static String usersAssistants({final bool personal = false}) => diff --git a/lib/views/ai/ai_chat_page.dart b/lib/views/ai/ai_chat_page.dart index c1b98ba..1cac369 100644 --- a/lib/views/ai/ai_chat_page.dart +++ b/lib/views/ai/ai_chat_page.dart @@ -1,4 +1,3 @@ -// 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'; @@ -50,7 +49,7 @@ class AiChatPage extends StatefulWidget { _AiChatPageState createState() => _AiChatPageState(); } -class _AiChatPageState extends State { +class _AiChatPageState extends State with TickerProviderStateMixin { final GlobalKey scaffKey = GlobalKey(); FocusNode focusNode = FocusNode(); BotsModel? _searchBot; @@ -60,6 +59,11 @@ class _AiChatPageState extends State { AiModel _selectedModel = AiModel.chatGPT; late BotsModel _currentBot; + late AnimationController _fadeController; + late AnimationController _messageController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + @override void didUpdateWidget(covariant AiChatPage oldWidget) { super.didUpdateWidget(oldWidget); @@ -70,7 +74,7 @@ class _AiChatPageState extends State { debugPrint("---------------------------------"); setState(() { _currentBot = widget.args.bot; - _isSearchMode = _currentBot.id == 36; // Reset search mode + _isSearchMode = _currentBot.id == 36; }); final state = context.read(); state.clearChat(); @@ -98,6 +102,33 @@ class _AiChatPageState extends State { @override void initState() { super.initState(); + + _fadeController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _messageController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + + _fadeAnimation = CurvedAnimation( + parent: _fadeController, + curve: Curves.easeIn, + ); + + _slideAnimation = Tween( + begin: const Offset(0, 0.1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _messageController, + curve: Curves.easeOutCubic, + )); + + _fadeController.forward(); + _messageController.forward(); + debugPrint("--- AI CHAT PAGE: Init ---"); debugPrint( "Bot ID: ${widget.args.bot.id}, Bot Name: ${widget.args.bot.name}"); @@ -116,24 +147,46 @@ class _AiChatPageState extends State { setState(() { _searchBot = bots.firstWhere((b) => b.id == 36); try { - _geminiBot = bots.firstWhere((b) => b.id == 35 && (b.name?.toLowerCase().contains('gemini') ?? false)); + _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'], + 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)); + _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'], + attachmentType: [ + 'audio', + 'image', + 'pdf', + 'csv', + 'doc', + 'docx', + 'xls', + 'xlsx' + ], attachment: 1, ); } @@ -146,21 +199,48 @@ class _AiChatPageState extends State { id: 36, name: 'GPT-4o-mini-search', responseType: 'text', - attachmentType: ['audio', 'image', 'pdf', 'csv', 'doc', 'docx', 'xls', 'xlsx'], + 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'], + 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'], + attachmentType: [ + 'audio', + 'image', + 'pdf', + 'csv', + 'doc', + 'docx', + 'xls', + 'xlsx' + ], attachment: 1, ); }); @@ -176,7 +256,6 @@ class _AiChatPageState extends State { const Duration(milliseconds: 100), () { focusNode.requestFocus(); - // اسکرول خودکار فقط در صورت وجود پیام if (state.messages.isNotEmpty) { state.scrollController.animateTo( state.scrollController.position.maxScrollExtent, @@ -203,7 +282,6 @@ class _AiChatPageState extends State { 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, @@ -215,6 +293,13 @@ class _AiChatPageState extends State { }); } + @override + void dispose() { + _fadeController.dispose(); + _messageController.dispose(); + super.dispose(); + } + BotsModel _getSelectedBot() { switch (_selectedModel) { case AiModel.gemini: @@ -235,13 +320,10 @@ 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( @@ -252,13 +334,10 @@ class _AiChatPageState extends State { context.read().getChats(); context.read().refresh = false; } - // اول state را reset می‌کنیم context.read().endChat(); - // صبر می‌کنیم تا state تغییر کند await Future.delayed(const Duration(milliseconds: 50)); - // حالا صفحه را می‌بندیم if (context.mounted) { Navigator.pop(context); } @@ -290,141 +369,179 @@ class _AiChatPageState extends State { 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), + FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 90), + userProvider.isLoadingWelcome + ? const SizedBox(height: 20) + : userProvider.welcomeMessage != null + ? TweenAnimationBuilder( + duration: + const Duration(milliseconds: 800), + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.scale( + scale: 0.8 + (0.2 * value), + child: child, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.only( + bottom: 12.0, + left: 24, + right: 24), child: Center( child: DidvanText( - 'سوالی برای پیشنهاد وجود ندارد.', - fontSize: 12, - color: Theme.of(context) - .colorScheme - .caption, + userProvider.welcomeMessage!, + textAlign: TextAlign.center, + fontSize: 18, + fontWeight: FontWeight.bold, + color: const Color.fromARGB( + 255, 0, 126, 167), ), ), + ), + ) + : const SizedBox(height: 20), + TweenAnimationBuilder( + duration: const Duration(milliseconds: 1000), + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, 20 * (1 - value)), + child: child, + ), + ); + }, + child: 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, + ), ) - : 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( + : 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 TweenAnimationBuilder< + double>( + duration: Duration( + milliseconds: 400 + + (index * 100)), + tween: Tween( + begin: 0.0, end: 1.0), + curve: Curves.easeOut, + builder: (context, value, + child) { + return Opacity( + opacity: value, + child: + Transform.translate( + offset: Offset( + 30 * (1 - value), + 0), + child: child, + ), + ); + }, + child: 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, - finished: true, fileLocal: null, + finished: true, role: 'user', createdAt: DateTime .now() @@ -433,69 +550,110 @@ class _AiChatPageState extends State { 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, - ); - }); + )); + } 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), + ), + ), + ], + ), + ), + ); }, - 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), // کاهش فضای خالی - ], + ), + const SizedBox(height: 10), + ], + ) + else + const SizedBox(height: 20), + ], + ), + ), ), if (state.messages.isNotEmpty) ListView.builder( @@ -529,7 +687,10 @@ class _AiChatPageState extends State { ), bottomNavigationBar: Padding( padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, + bottom: _currentBot.id == 35 + ? MediaQuery.of(context).viewInsets.bottom + : (MediaQuery.of(context).viewInsets.bottom - 72.0) + .clamp(0.0, double.infinity), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -540,8 +701,7 @@ class _AiChatPageState extends State { : _getSelectedBot(), attch: widget.args.attach, assistantsName: widget.args.assistantsName, - showSearchToggle: - _currentBot.id == 35 && _searchBot != null, + showSearchToggle: _currentBot.id == 35 && _searchBot != null, isSearchMode: _isSearchMode, onSearchModeToggled: (bool isSearchOn) { if (_searchBot == null) return; @@ -549,8 +709,9 @@ class _AiChatPageState extends State { _isSearchMode = isSearchOn; }); }, - showModelSelector: - _currentBot.id == 35 && _geminiBot != null && _grokBot != null, + showModelSelector: _currentBot.id == 35 && + _geminiBot != null && + _grokBot != null, selectedModel: _selectedModel, onModelChanged: (AiModel model) { setState(() { @@ -595,347 +756,389 @@ 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( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: message.role.toString().contains('user') - ? CrossAxisAlignment.start - : CrossAxisAlignment.end, - children: [ - Row( - mainAxisAlignment: message.role.toString().contains('user') - ? MainAxisAlignment.start - : MainAxisAlignment.end, - children: [ - Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.sizeOf(context).width / 1.3), - decoration: BoxDecoration( - borderRadius: DesignConfig.mediumBorderRadius.copyWith( - bottomLeft: !message.role.toString().contains('user') - ? Radius.zero - : null, - bottomRight: message.role.toString().contains('user') - ? Radius.zero - : null, + + final isUser = message.role.toString().contains('user'); + + return TweenAnimationBuilder( + duration: Duration(milliseconds: 300 + (index * 50)), + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(isUser ? -20 * (1 - value) : 20 * (1 - value), 0), + child: child, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: message.role.toString().contains('user') + ? CrossAxisAlignment.start + : CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: message.role.toString().contains('user') + ? MainAxisAlignment.start + : MainAxisAlignment.end, + children: [ + Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width / 1.3), + decoration: BoxDecoration( + borderRadius: DesignConfig.mediumBorderRadius.copyWith( + bottomLeft: !message.role.toString().contains('user') + ? Radius.zero + : null, + bottomRight: message.role.toString().contains('user') + ? Radius.zero + : null, + ), + color: message.error != null && message.error! + ? Theme.of(context).colorScheme.error.withOpacity(0.4) + : (message.role.toString().contains('user') + ? Theme.of(context).colorScheme.surface + : Theme.of(context).colorScheme.focused) + .withOpacity(0.9), + border: Border.all( + color: Theme.of(context).colorScheme.border, + width: 0.5, + ), ), - color: message.error != null && message.error! - ? Theme.of(context).colorScheme.error.withOpacity(0.4) - : (message.role.toString().contains('user') - ? Theme.of(context).colorScheme.surface - : Theme.of(context).colorScheme.focused) - .withOpacity(0.9), - border: Border.all( - color: Theme.of(context).colorScheme.border, - width: 0.5, - ), - ), - child: Container( - child: message.finished != null && !message.finished! - ? 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}...", - ), - ), - ); - }, - ); - }, - ), - Padding( - padding: - const EdgeInsets.symmetric(vertical: 8.0), - child: SpinKitThreeBounce( - color: Theme.of(context).colorScheme.primary, - size: 18, - ), - ), - ], - ) - : Column( - children: [ - if (file != null) - (file.isAudio()) - ? Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 8), - child: AudioWave( - file: file.path, - totalDuration: file.duration, - ), - ) - : file.isVideo() - ? Padding( - padding: const EdgeInsets.fromLTRB( - 16, 16, 16, 0), - child: ClipRRect( - borderRadius: - DesignConfig.lowBorderRadius, - child: ChatVideoPlayer( - src: RequestHelper.baseUrl + - file.path, - custome: const CustomControls(), - ), + child: Container( + child: message.finished != null && !message.finished! + ? 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}...", ), - ) - : file.isImage() - ? Padding( - padding: - const EdgeInsets.all(8.0), - child: messageImage(file), - ) - : Padding( - padding: - const EdgeInsets.all(8.0), - child: messageFile( - context, message, state), - ), - if (message.text != null && - message.text!.isNotEmpty && - ((message.audio == null || - (message.audio != null && - !message.audio!)))) + ), + ); + }, + ); + }, + ), Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16), - child: Directionality( - textDirection: message.text - .toString() - .startsWithEnglish() - ? TextDirection.ltr - : TextDirection.rtl, - child: DidvanMarkdownText( - text: message.text.toString(), - ), + padding: + const EdgeInsets.symmetric(vertical: 8.0), + child: SpinKitThreeBounce( + color: Theme.of(context).colorScheme.primary, + size: 18, ), ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (message.role - .toString() - .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() - .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, - ), - ), - ), - ], - ), + ], + ) + : Column( + children: [ + if (file != null) + (file.isAudio()) + ? Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: AudioWave( + file: file.path, + totalDuration: file.duration, + ), + ) + : file.isVideo() + ? Padding( + padding: const EdgeInsets.fromLTRB( + 16, 16, 16, 0), + child: ClipRRect( + borderRadius: + DesignConfig.lowBorderRadius, + child: ChatVideoPlayer( + src: RequestHelper.baseUrl + + file.path, + custome: const CustomControls(), ), ), - ), - ]; - }, - child: const SizedBox(), - ), - if (message.role - .toString() - .contains('user') && - index == - state.messages[mIndex].prompts - .length - - 2 && - (_currentBot.editable != null && - _currentBot.editable!)) - Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - onTap: () async { - state.isEdite = true; - state.message.text = - message.text.toString(); - state.update(); - }, - child: Icon( - Icons.edit_outlined, - size: 18, - color: Theme.of(context) - .colorScheme - .focusedBorder, - ), - ), - ), - if (message.file != null && kIsWeb) - Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - onTap: () async { - final url = - '${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}'; - MediaService.downloadFileFromWeb(url); - }, - child: Icon( - DidvanIcons.download_solid, - size: 18, - color: Theme.of(context) - .colorScheme - .focusedBorder, - ), - ), - ), - if (message.file != null && !kIsWeb) - Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - onTap: () async { - debugPrint( - "Download button tapped on iOS"); - final url = - '${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}'; - await MediaService.downloadFile(url, - name: message.fileName); - }, - child: Icon( - DidvanIcons.download_solid, - size: 18, - color: Theme.of(context) - .colorScheme - .focusedBorder, - ), - ), - ), - if (message.error != null && message.error!) - Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - onTap: () async { - state.messages.last.prompts - .remove(message); - state.messages.last.prompts.add( - message.copyWith(error: false)); - state.file = file; - state.update(); - final botToUse = _isSearchMode && - _searchBot != null - ? _searchBot! - : _getSelectedBot(); - await state.postMessage( - 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, - size: 18, - color: Theme.of(context) - .colorScheme - .focusedBorder, - ), - ), - ), - if (message.text != null && - message.text!.isNotEmpty && - (file == null || !file.isImage())) - Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - onTap: () async { - await Clipboard.setData(ClipboardData( - text: state.messages[mIndex] - .prompts[index].text - .toString())); - Future.delayed( - Duration.zero, - () => ActionSheetUtils(context) - .showAlert( - AlertData( - message: "متن با موفقیت کپی شد", - aLertType: ALertType.success, + ) + : file.isImage() + ? Padding( + padding: + const EdgeInsets.all(8.0), + child: messageImage(file), + ) + : Padding( + padding: + const EdgeInsets.all(8.0), + child: messageFile( + context, message, state), ), + if (message.text != null && + message.text!.isNotEmpty && + ((message.audio == null || + (message.audio != null && + !message.audio!)))) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 16), + child: Directionality( + textDirection: message.text + .toString() + .startsWithEnglish() + ? TextDirection.ltr + : TextDirection.rtl, + child: DidvanMarkdownText( + text: message.text.toString(), + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (message.role + .toString() + .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, + ), + ), + ), + ], + ), + ), + ), + ), + ]; + }, + child: const SizedBox(), + ), + if (message.role + .toString() + .contains('user') && + index == + state.messages[mIndex].prompts + .length - + 2 && + (_currentBot.editable != null && + _currentBot.editable!)) + Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () async { + state.isEdite = true; + state.message.text = + message.text.toString(); + state.update(); + }, + child: Icon( + Icons.edit_outlined, + size: 18, + color: Theme.of(context) + .colorScheme + .focusedBorder, + ), + ), + ), + if (message.file != null && kIsWeb) + Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () async { + final url = + '${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}'; + MediaService.downloadFileFromWeb( + url); + }, + child: Icon( + DidvanIcons.download_solid, + size: 18, + color: Theme.of(context) + .colorScheme + .focusedBorder, + ), + ), + ), + if (message.file != null && !kIsWeb) + Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () async { + debugPrint( + "Download button tapped on iOS"); + final url = + '${RequestHelper.baseUrl + message.file.toString()}?accessToken=${RequestService.token}'; + await MediaService.downloadFile(url, + name: message.fileName); + }, + child: Icon( + DidvanIcons.download_solid, + size: 18, + color: Theme.of(context) + .colorScheme + .focusedBorder, + ), + ), + ), + if (message.error != null && message.error!) + Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () async { + state.messages.last.prompts + .remove(message); + state.messages.last.prompts.add( + message.copyWith(error: false)); + state.file = file; + state.update(); + final botToUse = _isSearchMode && + _searchBot != null + ? _searchBot! + : _getSelectedBot(); + await state.postMessage( + 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, + size: 18, + color: Theme.of(context) + .colorScheme + .focusedBorder, + ), + ), + ), + if (message.text != null && + message.text!.isNotEmpty && + (file == null || !file.isImage())) + Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () async { + await Clipboard.setData( + ClipboardData( + text: state.messages[mIndex] + .prompts[index].text + .toString())); + Future.delayed( + Duration.zero, + () => ActionSheetUtils(context) + .showAlert( + AlertData( + message: + "متن با موفقیت کپی شد", + aLertType: ALertType.success, + ), + ), + ); + }, + child: Icon( + DidvanIcons.copy_regular, + size: 18, + color: Theme.of(context) + .colorScheme + .focusedBorder, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () async { + if (message.id != null) { + state.deleteMessage( + message.id!, mIndex, index); + } else { + state.messages[mIndex].prompts + .removeAt(index); + state.update(); + } + }, child: Icon( - DidvanIcons.copy_regular, + DidvanIcons.trash_solid, size: 18, color: Theme.of(context) .colorScheme @@ -943,50 +1146,29 @@ class _AiChatPageState extends State { ), ), ), - Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - onTap: () async { - if (message.id != null) { - state.deleteMessage( - message.id!, mIndex, index); - } else { - state.messages[mIndex].prompts - .removeAt(index); - state.update(); - } - }, - child: Icon( - DidvanIcons.trash_solid, - size: 18, - color: Theme.of(context) - .colorScheme - .focusedBorder, - ), - ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 8), - ], - ), + const SizedBox(height: 8), + ], + ), + ), ), - ), - ], - ), - const SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - DidvanText( - DateTimeUtils.timeWithAmPm(message.createdAt.toString()), - style: Theme.of(context).textTheme.labelSmall, - color: Theme.of(context).colorScheme.caption, - ), - ], - ), - ], + ], + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + DidvanText( + DateTimeUtils.timeWithAmPm(message.createdAt.toString()), + style: Theme.of(context).textTheme.labelSmall, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + ], + ), ), ); } diff --git a/lib/views/ai/ai_chat_state.dart b/lib/views/ai/ai_chat_state.dart index f5ba32c..11774d7 100644 --- a/lib/views/ai/ai_chat_state.dart +++ b/lib/views/ai/ai_chat_state.dart @@ -1,5 +1,3 @@ -// lib/views/ai/ai_chat_state.dart - // ignore_for_file: body_might_complete_normally_catch_error, avoid_print import 'dart:async'; @@ -39,10 +37,8 @@ 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 { @@ -80,23 +76,18 @@ 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(); @@ -104,13 +95,11 @@ class AiChatState extends CoreProvier { 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; @@ -190,16 +179,18 @@ class AiChatState extends CoreProvier { .toIso8601String())); update(); await _scrolledEnd(); - - // تعیین URL بر اساس bot ID + String url; if (bot.id == 100) { - // برای Aisummery از endpoint خاص استفاده می‌کنیم url = '/100/aisummery'; + } else if (bot.id == 101) { + url = '/101/aiaudio'; + } else if (bot.id == 27) { + url = '/27/chart-analysis'; } else { - // برای بقیه bot ها از endpoint عادی استفاده می‌کنیم - url = '${isAssistants ? '/user/${bot.responseType}' : ''}/${bot.id}/${bot.name}' - .toLowerCase(); + url = + '${isAssistants ? '/user/${bot.responseType}' : ''}/${bot.id}/${bot.name}' + .toLowerCase(); } final req = await AiApiService.initial( @@ -235,7 +226,15 @@ class AiChatState extends CoreProvier { } } + if (bot.id == 101) { + print('🎵 TTS Stream - Raw data: $str'); + responseMessgae += str; + print('🎵 TTS Stream - Full responseMessgae: $responseMessgae'); + return; + } + responseMessgae += str; + if (kIsWeb) { try { int startIndex = responseMessgae.indexOf('{{{'); @@ -278,18 +277,64 @@ class AiChatState extends CoreProvier { } else { int? humanMessageId; int? aiMessageId; - try { - final data = AppInitializer.messagesData(dataMessgae); - humanMessageId = data['HUMAN_MESSAGE_ID']; - aiMessageId = data['AI_MESSAGE_ID']; - } catch (e) { - e.printError(); - return; + String? audioUrl; + + if (bot.id == 101) { + print('🎵 TTS onDone - responseMessgae: $responseMessgae'); + print('🎵 TTS onDone - dataMessgae: $dataMessgae'); } + + if (bot.id == 101) { + try { + print('🎵 TTS - Parsing responseMessgae directly...'); + String jsonString = responseMessgae; + + if (jsonString.contains('}{')) { + jsonString = + jsonString.substring(0, jsonString.indexOf('}{') + 1); + print('🎵 TTS - Extracted first JSON: $jsonString'); + } + + final jsonData = json.decode(jsonString); + print('🎵 TTS - Parsed JSON: $jsonData'); + + audioUrl = jsonData['url']?.toString(); + humanMessageId = jsonData['HUMAN_MESSAGE_ID']; + aiMessageId = jsonData['AI_MESSAGE_ID']; + print('🎵 TTS - audioUrl: $audioUrl'); + print( + '🎵 TTS - humanMessageId: $humanMessageId, aiMessageId: $aiMessageId'); + } catch (e) { + print('🎵 TTS - ERROR parsing: $e'); + e.printError(); + return; + } + } else { + try { + final data = AppInitializer.messagesData(dataMessgae); + humanMessageId = data['HUMAN_MESSAGE_ID']; + aiMessageId = data['AI_MESSAGE_ID']; + } catch (e) { + e.printError(); + return; + } + } + + if (bot.id == 101) { + print('🎵 TTS - Saving message:'); + print('🎵 TTS - text: ${bot.id == 101 ? null : responseMessgae}'); + print('🎵 TTS - file: ${bot.id == 101 ? audioUrl : responseMessgae}'); + print('🎵 TTS - bot.responseType: ${bot.responseType}'); + } + messages.last.prompts.last = messages.last.prompts.last.copyWith( finished: true, - text: bot.responseType != 'text' ? null : responseMessgae, - file: bot.responseType != 'text' ? responseMessgae : null, + text: bot.responseType != 'text' && bot.id != 101 + ? null + : (bot.id == 101 ? null : responseMessgae), + file: bot.responseType != 'text' || bot.id == 101 + ? (bot.id == 101 ? audioUrl : responseMessgae) + : null, id: aiMessageId); if (messages.last.prompts.length > 2) { messages.last.prompts[messages.last.prompts.length - 2] = messages @@ -346,4 +391,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 b57dec9..2cb222c 100644 --- a/lib/views/ai/widgets/ai_message_bar.dart +++ b/lib/views/ai/widgets/ai_message_bar.dart @@ -1,5 +1,3 @@ -// 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'; @@ -38,7 +36,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 +import 'package:flutter_svg/flutter_svg.dart'; typedef _Fn = void Function(); @@ -46,28 +44,24 @@ class AiMessageBar extends StatefulWidget { final BotsModel bot; final String? assistantsName; final bool? attch; - // --- 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 @@ -256,7 +250,6 @@ 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( @@ -279,14 +272,10 @@ class _AiMessageBarState extends State { width: 1.5), borderRadius: BorderRadius.circular(16), ), - // --- MODIFIED: Changed Row to Column --- child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // --- MODIFIED: Added new Web Search Toggle --- - - // --- MODIFIED: This is the original Row, now inside the Column --- Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ @@ -311,7 +300,6 @@ class _AiMessageBarState extends State { 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) @@ -329,7 +317,6 @@ class _AiMessageBarState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - // Model Selector Dropdown (3 options) if (widget.showModelSelector) Builder( builder: (context) { @@ -338,16 +325,18 @@ class _AiMessageBarState extends State { switch (widget.selectedModel) { case AiModel.gemini: - icon = ''; + icon = + 'lib/assets/icons/Google-gemini-icon.svg'; label = 'Gemini'; break; case AiModel.grok: - icon = 'assets/icons/grok.svg'; + icon = 'lib/assets/icons/grok.svg'; label = 'Grok'; break; case AiModel.chatGPT: default: - icon = 'assets/icons/chat_gpt.svg'; + icon = + 'lib/assets/icons/ChatGPT-Logo.svg'; label = 'ChatGPT'; } @@ -365,9 +354,9 @@ class _AiMessageBarState extends State { value: AiModel.chatGPT, child: Row( children: [ - Icon( - Icons.chat_bubble_outline, - size: 20, + SvgPicture.asset( + 'lib/assets/icons/ChatGPT-Logo.svg', + height: 17, ), const SizedBox(width: 12.0), const DidvanText( @@ -377,32 +366,32 @@ class _AiMessageBarState extends State { ], ), ), - const PopupMenuItem( + PopupMenuItem( value: AiModel.gemini, child: Row( children: [ - Icon( - Icons.auto_awesome, - size: 20, + SvgPicture.asset( + 'lib/assets/icons/Google-gemini-icon.svg', + height: 17, ), - SizedBox(width: 12.0), - DidvanText( + const SizedBox(width: 12.0), + const DidvanText( 'Gemini', fontSize: 14, ), ], ), ), - const PopupMenuItem( + PopupMenuItem( value: AiModel.grok, child: Row( children: [ - Icon( - Icons.bolt, - size: 20, + SvgPicture.asset( + 'lib/assets/icons/grok.svg', + height: 17, ), - SizedBox(width: 12.0), - DidvanText( + const SizedBox(width: 12.0), + const DidvanText( 'Grok', fontSize: 14, ), @@ -422,7 +411,7 @@ class _AiMessageBarState extends State { children: [ SvgPicture.asset( icon, - height: 20, + height: 17, ), const SizedBox(width: 8.0), DidvanText( @@ -432,30 +421,26 @@ class _AiMessageBarState extends State { 255, 61, 61, 61), ), const SizedBox(width: 4.0), - const Icon( - Icons.arrow_drop_down, - size: 20, - color: Color.fromARGB( - 255, 61, 61, 61), - ), + Center( + child: SvgPicture.asset( + 'lib/assets/icons/eva_arrow-down-fill.svg', + height: 18, + ), + ) ], ), ), ); }, ), - - // 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); + const Color activeColor = + Color.fromARGB(255, 0, 126, 167); final bool isSearchMode = widget.isSearchMode; final Color backgroundColor = isSearchMode @@ -512,10 +497,6 @@ class _AiMessageBarState extends State { ) ], ), - - // --- 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( padding: EdgeInsets.fromLTRB(3, 8, 3, 4), @@ -629,7 +610,7 @@ class _AiMessageBarState extends State { Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), child: MessageBarBtn( - enable: true, // <-- فعال کردن دکمه + enable: true, icon: openAttach || state.file != null ? DidvanIcons.close_regular : Icons.add, @@ -657,15 +638,13 @@ class _AiMessageBarState extends State { textInputAction: TextInputAction.newline, style: Theme.of(context).textTheme.bodyMedium, minLines: 1, - maxLines: 6, // Set this + maxLines: 6, keyboardType: TextInputType.multiline, controller: state.message, - - enabled: !(state.file != null && widget.bot.attachment == 1), + enabled: true, decoration: InputDecoration( 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 @@ -708,14 +687,13 @@ class _AiMessageBarState extends State { ), click: getRecorderFn(), ) - // --- MODIFIED BLOCK: استفاده از iconWidget برای SVG --- : MessageBarBtn( enable: (state.file != null && state.file!.isRecorded) || (widget.bot.attachment == 1) || message.text.isNotEmpty, iconWidget: SvgPicture.asset( - 'lib/assets/icons/send3.svg', // <-- مسیر SVG شما - width: 24, // اندازه دلخواه + 'lib/assets/icons/send3.svg', + width: 24, height: 24, color: const Color.fromARGB(255, 0, 126, 167), ), @@ -768,12 +746,10 @@ 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 --- ), ); } @@ -897,8 +873,7 @@ class _AiMessageBarState extends State { String? name = result.files.first.name; if (kIsWeb) { - Uint8List? bytes = result - .files.first.bytes; // Access the bytes property + Uint8List? bytes = result.files.first.bytes; final blob = html.Blob([bytes]); final blobUrl = html.Url.createObjectUrlFromBlob(blob); @@ -929,7 +904,6 @@ class _AiMessageBarState extends State { )); } - // --- MODIFIED BLOCK: کد صحیح تابع --- Widget audioContainer() { final state = context.watch(); @@ -937,12 +911,11 @@ 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/message_bar_btn.dart b/lib/views/ai/widgets/message_bar_btn.dart index 595ceaa..c864258 100644 --- a/lib/views/ai/widgets/message_bar_btn.dart +++ b/lib/views/ai/widgets/message_bar_btn.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; class MessageBarBtn extends StatelessWidget { final bool enable; final IconData? icon; - final Widget? iconWidget; // برای پذیرش ویجت‌های سفارشی + final Widget? iconWidget; final Function()? click; const MessageBarBtn({ @@ -14,24 +14,19 @@ class MessageBarBtn extends StatelessWidget { this.click, required this.enable, }) : assert(icon != null || iconWidget != null, - 'Either icon or iconWidget must be provided'), // اطمینان از اینکه یکی از آیکن‌ها ارائه شده + 'Either icon or iconWidget must be provided'), super(key: key); @override Widget build(BuildContext context) { - // --- BLOCK MODIFIED --- - // ویجت آیکن نهایی را مشخص می‌کنیم Widget finalIconWidget; if (iconWidget != null) { - // اگر ویجت سفارشی (SVG) داریم، رنگ اصلی‌اش را حفظ می‌کنیم - // و فقط برای حالت غیرفعال، آن را کم‌رنگ می‌کنیم finalIconWidget = Opacity( - opacity: enable ? 1.0 : 0.5, // شفافیت بر اساس فعال بودن + opacity: enable ? 1.0 : 0.5, child: iconWidget, ); } else { - // اگر آیکن معمولی (IconData) داریم، از رنگ تم استفاده می‌کنیم final color = enable ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.disabledText; @@ -39,16 +34,15 @@ class MessageBarBtn extends StatelessWidget { finalIconWidget = Icon( icon, size: 24, - color: color, // رنگ بر اساس فعال/غیرفعال بودن + color: color, ); } - // --- END MODIFIED BLOCK --- return InkWell( onTap: click, child: Padding( padding: const EdgeInsets.all(4.0), - child: finalIconWidget, // نمایش ویجت آیکن نهایی + child: finalIconWidget, ), ); } diff --git a/lib/views/ai/widgets/tool_category_view_widget.dart b/lib/views/ai/widgets/tool_category_view_widget.dart index a5d04a8..48739b8 100644 --- a/lib/views/ai/widgets/tool_category_view_widget.dart +++ b/lib/views/ai/widgets/tool_category_view_widget.dart @@ -1,5 +1,3 @@ -// lib/views/ai/widgets/tool_category_view_widget.dart - import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; @@ -119,8 +117,7 @@ class ToolCategoryViewWidget extends StatelessWidget { ), Expanded( child: Padding( - padding: - const EdgeInsets.fromLTRB(12, 8, 12, 12), + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -168,9 +165,8 @@ class ToolCategoryViewWidget extends StatelessWidget { child: DidvanText( bot.short!, fontSize: 10, - color: Theme.of(context) - .colorScheme - .caption, + color: + Theme.of(context).colorScheme.caption, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, @@ -199,4 +195,4 @@ class ToolCategoryViewWidget extends StatelessWidget { ), ); } -} \ 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 5bbbea1..7812821 100644 --- a/lib/views/ai_section/ai_section_page.dart +++ b/lib/views/ai_section/ai_section_page.dart @@ -21,24 +21,54 @@ class AiSectionPage extends StatefulWidget { class _AiSectionGridItem { final String title; - final String description; // فیلد جدید اضافه شد + final String description; final String iconPath; final void Function(BuildContext context) onTap; _AiSectionGridItem({ required this.title, - required this.description, // به سازنده اضافه شد + required this.description, required this.iconPath, required this.onTap, }); } -class _AiSectionPageState extends State { +class _AiSectionPageState extends State with TickerProviderStateMixin { late final List<_AiSectionGridItem> _gridItems; + late AnimationController _fadeController; + late AnimationController _slideController; + late Animation _fadeAnimation; + late Animation _slideAnimation; @override void initState() { super.initState(); + + _fadeController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _fadeAnimation = CurvedAnimation( + parent: _fadeController, + curve: Curves.easeIn, + ); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + + _slideAnimation = Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutCubic, + )); + + _fadeController.forward(); + _slideController.forward(); WidgetsBinding.instance.addPostFrameCallback((_) { final historyAiChatState = context.read(); if (historyAiChatState.bots.isEmpty) { @@ -61,7 +91,7 @@ class _AiSectionPageState extends State { _gridItems = [ _AiSectionGridItem( title: 'ساخت عکس', - description: 'ایجاد تصاویر خلاقانه با هوش مصنوعی', // توضیحات اضافه شد + description: 'ایجاد تصاویر خلاقانه با هوش مصنوعی', iconPath: 'lib/assets/icons/create image.svg', onTap: (context) { final aiState = context.read(); @@ -80,7 +110,7 @@ class _AiSectionPageState extends State { ), _AiSectionGridItem( title: 'ترجمه', - description: 'ترجمه متون به زبان‌های مختلف', // توضیحات اضافه شد + description: 'ترجمه متون به زبان‌های مختلف', iconPath: 'lib/assets/icons/translate.svg', onTap: (context) { final aiState = context.read(); @@ -99,13 +129,12 @@ class _AiSectionPageState extends State { ), _AiSectionGridItem( title: 'خلاصه‌ساز', - description: 'ساخت نمودار با تحلیل هوشمند', + description: 'خلاصه‌سازی متن با هوش مصنوعی', iconPath: 'lib/assets/icons/summary.svg', onTap: (context) { final aiState = context.read(); aiState.endChat(); - // ساخت BotsModel برای Aisummery final aisummeryBot = BotsModel( id: 100, name: 'Aisummery', @@ -117,9 +146,54 @@ class _AiSectionPageState extends State { aiState.startChat(AiChatArgs(bot: aisummeryBot)); }, ), + _AiSectionGridItem( + title: 'متن به صوت', + description: 'تبدیل متن به فایل صوتی', + iconPath: 'lib/assets/icons/text to voice.svg', + onTap: (context) { + final aiState = context.read(); + aiState.endChat(); + + final textToSpeechBot = BotsModel( + id: 101, + name: 'aiaudio', + responseType: 'audio', + attachmentType: [], + attachment: 0, + ); + + aiState.startChat(AiChatArgs(bot: textToSpeechBot)); + }, + ), + _AiSectionGridItem( + title: 'تحلیل و ترسیم نمودار', + description: 'ساخت نمودار با تحلیل هوشمند', + iconPath: 'lib/assets/icons/chart-analysis.svg', + onTap: (context) { + final aiState = context.read(); + aiState.endChat(); + + final chartAnalysisBot = BotsModel( + id: 27, + name: 'chart-analysis', + responseType: 'text', + attachmentType: ['image', 'pdf'], + attachment: 1, + ); + + aiState.startChat(AiChatArgs(bot: chartAnalysisBot)); + }, + ), ]; } + @override + void dispose() { + _fadeController.dispose(); + _slideController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final aiState = context.watch(); @@ -136,23 +210,32 @@ class _AiSectionPageState extends State { 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, - ), - ], + FadeTransition( + opacity: _fadeAnimation, + child: 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), + SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: SizedBox( + height: MediaQuery.of(context).size.height - 100, + child: _buildAiGrid(context, aiState), + ), + ), ), ], ), @@ -179,28 +262,56 @@ class _AiSectionPageState extends State { itemBuilder: (context, columnIndex) { return Padding( padding: EdgeInsets.only( - left: columnIndex == (_gridItems.length / 2).ceil() - 1 ? 0 : 16.0, + left: columnIndex == (_gridItems.length / 2).ceil() - 1 ? 0 : 4.0, right: 16.0, ), child: SizedBox( width: 180, - height: 340, // ارتفاع افزایش یافت (قبلا 300) + height: 340, child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ if (columnIndex * 2 < _gridItems.length) - SizedBox( - width: 180, - height: 160, // ارتفاع کارت افزایش یافت (قبلا 140) - child: _buildGridItemCard(context, _gridItems[columnIndex * 2]), + TweenAnimationBuilder( + duration: Duration(milliseconds: 600 + (columnIndex * 100)), + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(50 * (1 - value), 0), + child: child, + ), + ); + }, + child: SizedBox( + width: 180, + height: 160, + 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]), + child: TweenAnimationBuilder( + duration: Duration(milliseconds: 700 + (columnIndex * 100)), + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.easeOut, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(50 * (1 - value), 0), + child: child, + ), + ); + }, + child: SizedBox( + width: 180, + height: 160, + child: _buildGridItemCard(context, _gridItems[columnIndex * 2 + 1]), + ), ), ), ], @@ -212,48 +323,111 @@ class _AiSectionPageState extends State { } Widget _buildGridItemCard(BuildContext context, _AiSectionGridItem item) { - return InkWell( - onTap: () => item.onTap(context), - borderRadius: BorderRadius.circular(25), - child: Container( - decoration: BoxDecoration( - 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.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.asset( - item.iconPath, - width: 48, - height: 48, - ), - const SizedBox(height: 8), - DidvanText( - item.title, - style: Theme.of(context).textTheme.titleSmall, - 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, + return _AnimatedGridCard(item: item); + } +} + +class _AnimatedGridCard extends StatefulWidget { + final _AiSectionGridItem item; + + const _AnimatedGridCard({required this.item}); + + @override + State<_AnimatedGridCard> createState() => _AnimatedGridCardState(); +} + +class _AnimatedGridCardState extends State<_AnimatedGridCard> { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 400), + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.easeOutBack, + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: child, + ); + }, + child: MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: InkWell( + onTap: () => widget.item.onTap(context), + borderRadius: BorderRadius.circular(25), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + transform: Matrix4.identity() + ..translate(0.0, _isHovered ? -8.0 : 0.0), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 245, 245, 245), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: _isHovered + ? const Color.fromARGB(255, 0, 126, 167) + : const Color.fromARGB(255, 184, 184, 184), + width: _isHovered ? 2 : 1, ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(_isHovered ? 0.15 : 0.05), + blurRadius: _isHovered ? 20 : 10, + offset: Offset(0, _isHovered ? 8 : 4), + ), + ], ), - ], + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TweenAnimationBuilder( + duration: const Duration(milliseconds: 600), + tween: Tween(begin: 0.0, end: 1.0), + curve: Curves.elasticOut, + builder: (context, value, child) { + return Transform.rotate( + angle: (1 - value) * 0.5, + child: Opacity( + opacity: value, + child: child, + ), + ); + }, + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: _isHovered ? 1.1 : 1.0, + child: SvgPicture.asset( + widget.item.iconPath, + width: 48, + height: 48, + ), + ), + ), + const SizedBox(height: 8), + DidvanText( + widget.item.title, + style: Theme.of(context).textTheme.titleSmall, + color: const Color.fromARGB(255, 0, 126, 167), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Expanded( + child: DidvanText( + widget.item.description, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Theme.of(context).colorScheme.caption, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), ), ), ); diff --git a/lib/views/home/explore/explore.dart b/lib/views/home/explore/explore.dart index 6ad2e01..7fd6239 100644 --- a/lib/views/home/explore/explore.dart +++ b/lib/views/home/explore/explore.dart @@ -3,10 +3,8 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/main.dart'; import 'package:didvan/models/home_page_content/home_page_list.dart'; import 'package:didvan/models/home_page_content/swot.dart'; -import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/app_initalizer.dart'; import 'package:didvan/views/home/main/main_page_state.dart'; -import 'package:didvan/views/home/main/widgets/banner.dart'; import 'package:didvan/views/home/main/widgets/general_item.dart'; import 'package:didvan/views/home/main/widgets/podcast_item.dart'; import 'package:didvan/views/widgets/didvan/slider.dart'; @@ -20,8 +18,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:didvan/views/home/main/widgets/swot_item_card.dart'; import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform; import 'package:universal_html/html.dart' as html; - -import 'package:didvan/views/home/main/main_page.dart'; +import 'package:didvan/views/widgets/home_app_bar.dart'; bool isAnyMobile() { if (kIsWeb) { @@ -47,54 +44,20 @@ class ExplorePage extends StatelessWidget { state: state, builder: (context, state) { final List pageContent = []; + + pageContent.add(const HomeAppBar( + showSearchField: true, + )); + if (state.content != null && state.content!.lists.isNotEmpty) { final lists = state.content!.lists; for (int i = 0; i < lists.length; i++) { final currentList = lists[i]; - if (i == 4) { - pageContent.add( - Padding( - padding: const EdgeInsets.only(top: 32), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - bottom: 16, - top: 28, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const InfoTitle(), - GestureDetector( - onTap: () => { - Navigator.of(context) - .pushNamed(Routes.infography) - }, - child: Row( - children: [ - DidvanText( - "مشاهده همه", - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ], - ), - ) - ], - ), - ), - const MainPageBanner( - isFirst: false, - ), - ], - ), - ), - ); + if (currentList.type == 'video' || + currentList.type == 'podcast') { + continue; } pageContent.add(MainPageSection( @@ -119,7 +82,7 @@ class ExplorePage extends StatelessWidget { class SwotSection extends StatelessWidget { final List swotItems; - const SwotSection({required this.swotItems}); + const SwotSection({super.key, required this.swotItems}); @override Widget build(BuildContext context) { @@ -182,7 +145,7 @@ class SwotSection extends StatelessWidget { /// Swot Items Slider DidvanSlider( height: 330, - itemCount: 7, + itemCount: 7, // TODO: This should probably be swotItems.length viewportFraction: isAnyMobile() ? 0.65 : 0.55, itemBuilder: (context, index, realIndex) => Padding( padding: const EdgeInsets.symmetric(horizontal: 0.0), @@ -198,28 +161,6 @@ class SwotSection extends StatelessWidget { } } -class InfoTitle extends StatelessWidget { - const InfoTitle({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Icon( - DidvanIcons.infography_solid, - color: Theme.of(context).colorScheme.title, - ), - const SizedBox(width: 4), - DidvanText( - "اینفوگرافی", - style: Theme.of(context).textTheme.titleMedium, - color: Theme.of(context).colorScheme.title, - ), - ], - ); - } -} - class MainPageSection extends StatelessWidget { final MainPageList list; final bool isLast; @@ -377,4 +318,4 @@ class MainPageSection extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/views/widgets/didvan/bnb.dart b/lib/views/widgets/didvan/bnb.dart index d4ab09b..fe95083 100644 --- a/lib/views/widgets/didvan/bnb.dart +++ b/lib/views/widgets/didvan/bnb.dart @@ -69,7 +69,7 @@ class DidvanBNB extends StatelessWidget { _NavBarItem( isSelected: currentTabIndex == 3, title: 'کاوش', - selectedIconPath: 'lib/assets/icons/discover_solid.svg', // Selected SVG icon + selectedIconPath: 'lib/assets/icons/explore select.svg', // Selected SVG icon unselectedIconPath: 'lib/assets/icons/discover.svg', // Unselected SVG icon onTap: () => onTabChanged(3), ), @@ -94,6 +94,7 @@ class _NavBarItem extends StatefulWidget { required this.selectedIconPath, required this.unselectedIconPath, required this.onTap, + // ignore: unused_element_parameter this.isHomeButton = false, }) : super(key: key); diff --git a/lib/views/widgets/home_app_bar.dart b/lib/views/widgets/home_app_bar.dart index 1f236a2..283e8bd 100644 --- a/lib/views/widgets/home_app_bar.dart +++ b/lib/views/widgets/home_app_bar.dart @@ -45,7 +45,10 @@ class HomeAppBar extends StatelessWidget { color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.2), + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.2), ), ), child: Center( @@ -63,42 +66,50 @@ class HomeAppBar extends StatelessWidget { height: 60, color: Theme.of(context).colorScheme.title, ), - if (title != null && showBackButton) Expanded( child: Center( child: Text( title!, style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.title, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.title, + ), + ), + ), + ), + Row( + children: [ + IconButton( + icon: const Icon(DidvanIcons.bookmark_regular), // + onPressed: () { + Navigator.of(context).pushNamed(Routes.bookmarks); // + }, + ), + GestureDetector( + onTap: () { + Navigator.pushNamed(context, Routes.profile); + }, + child: SizedBox( + width: 44, + height: 44, + child: Center( + child: SvgPicture.asset( + 'lib/assets/icons/New_Profile.svg', + width: 30, + height: 30, + ), ), ), ), - ), - - GestureDetector( - onTap: () { - Navigator.pushNamed(context, Routes.profile); - }, - child: SizedBox( - width: 44, - height: 44, - child: Center( - child: SvgPicture.asset( - 'lib/assets/icons/New_Profile.svg', - width: 30, - height: 30, - ), - ), - ), + ], ), ], ), - ).animate() - .fadeIn(duration: 500.ms) - .slideY(begin: -0.5, duration: 500.ms), - + ) + .animate() + .fadeIn(duration: 500.ms) + .slideY(begin: -0.5, duration: 500.ms), if (showSearchField) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), @@ -200,4 +211,4 @@ class HomeAppBar extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/views/widgets/hoshan_app_bar.dart b/lib/views/widgets/hoshan_app_bar.dart index 5c33b60..0dacfa1 100644 --- a/lib/views/widgets/hoshan_app_bar.dart +++ b/lib/views/widgets/hoshan_app_bar.dart @@ -1,9 +1,7 @@ -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'; @@ -13,7 +11,7 @@ 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); @@ -68,7 +66,7 @@ class HoshanAppBar extends StatelessWidget implements PreferredSizeWidget { // ) // ], // ), - + GestureDetector( onTap: () { print('history bottom tapped'); @@ -86,7 +84,9 @@ class HoshanAppBar extends StatelessWidget implements PreferredSizeWidget { ), ), ), - const SizedBox(width: 5,), + const SizedBox( + width: 5, + ), if (withInfo) DidvanIconButton( icon: DidvanIcons.angle_left_light, diff --git a/lib/views/widgets/hoshan_home_app_bar.dart b/lib/views/widgets/hoshan_home_app_bar.dart index 171c01d..7e414f7 100644 --- a/lib/views/widgets/hoshan_home_app_bar.dart +++ b/lib/views/widgets/hoshan_home_app_bar.dart @@ -1,15 +1,15 @@ 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/providers/user.dart'; import 'package:didvan/views/ai/history_ai_chat_state.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:didvan/routes/routes.dart'; +// ignore: depend_on_referenced_packages import 'package:shamsi_date/shamsi_date.dart'; -// import 'package:didvan/services/network/request.dart'; // --- REMOVED --- (دیگه نیازی نیست) class HoshanHomeAppBar extends StatefulWidget { const HoshanHomeAppBar({super.key}); @@ -19,7 +19,6 @@ class HoshanHomeAppBar extends StatefulWidget { } class _HoshanHomeAppBarState extends State { - // --- REMOVED --- // String? _welcomeMessage; // bool _isLoadingWelcome = true; @@ -30,7 +29,6 @@ class _HoshanHomeAppBarState extends State { // } // Future fetchWelcomeMessage() async { ... } - // --- END REMOVED --- void showHistoryDrawer(BuildContext context) { showGeneralDialog( @@ -82,9 +80,7 @@ class _HoshanHomeAppBarState extends State { @override Widget build(BuildContext context) { - // --- ADDED --- final userProvider = context.watch(); - // --- END ADDED --- return Container( decoration: const BoxDecoration( @@ -132,7 +128,6 @@ class _HoshanHomeAppBarState extends State { Container( padding: const EdgeInsets.fromLTRB(8, 35, 8, 35), alignment: Alignment.center, - // --- MODIFIED --- child: userProvider.isLoadingWelcome ? const SizedBox(height: 20) : userProvider.welcomeMessage != null @@ -143,7 +138,6 @@ class _HoshanHomeAppBarState extends State { color: Theme.of(context).colorScheme.title, ) : const SizedBox(height: 20), - // --- END MODIFIED --- ), GestureDetector( onTap: () { @@ -232,7 +226,7 @@ class _HoshanHomeAppBarState extends State { } class HistoryDrawerContent extends StatefulWidget { - const HistoryDrawerContent(); + const HistoryDrawerContent({super.key}); @override State createState() => _HistoryDrawerContentState(); @@ -314,6 +308,7 @@ class _HistoryDrawerContentState extends State { color: const Color.fromARGB(255, 237, 237, 237), boxShadow: [ BoxShadow( + // ignore: deprecated_member_use color: Colors.black.withOpacity(0.2), blurRadius: 20, offset: const Offset(5, 0), @@ -676,6 +671,7 @@ class _HistoryDrawerContentState extends State { SvgPicture.asset( 'lib/assets/icons/trash.svg', height: 20, + // ignore: deprecated_member_use color: const Color.fromARGB( 255, 0, 126, 167), ), @@ -765,4 +761,4 @@ class _HistoryDrawerContentState extends State { ), ); } -} \ No newline at end of file +}