added summary and voice model to houshan

This commit is contained in:
mohamadmahdi jebeli 2025-11-02 14:19:39 +03:30
parent e2c46a07c1
commit 00953b3a19
22 changed files with 1252 additions and 927 deletions

View File

@ -0,0 +1,3 @@
<svg viewBox='0 0 320 320' xmlns='http://www.w3.org/2000/svg'>
<path d='m297.06 130.97c7.26-21.79 4.76-45.66-6.85-65.48-17.46-30.4-52.56-46.04-86.84-38.68-15.25-17.18-37.16-26.95-60.13-26.81-35.04-.08-66.13 22.48-76.91 55.82-22.51 4.61-41.94 18.7-53.31 38.67-17.59 30.32-13.58 68.54 9.92 94.54-7.26 21.79-4.76 45.66 6.85 65.48 17.46 30.4 52.56 46.04 86.84 38.68 15.24 17.18 37.16 26.95 60.13 26.8 35.06.09 66.16-22.49 76.94-55.86 22.51-4.61 41.94-18.7 53.31-38.67 17.57-30.32 13.55-68.51-9.94-94.51zm-120.28 168.11c-14.03.02-27.62-4.89-38.39-13.88.49-.26 1.34-.73 1.89-1.07l63.72-36.8c3.26-1.85 5.26-5.32 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97zm-128.84-55.03c-7.03-12.14-9.56-26.37-7.15-40.18.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83l-64.41 37.19c-28.69 16.52-65.33 6.7-81.92-21.95zm-16.77-139.09c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91-26.93 15.55c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89zm221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94-7.01 12.14-18.05 21.44-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06zm26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8c-3.23-1.89-7.23-1.89-10.47 0l-77.79 44.92v-31.1c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22 6.99 12.12 9.52 26.31 7.15 40.1zm-168.51 55.43-26.94-15.55c-.29-.14-.48-.42-.52-.74v-74.39c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07l-63.72 36.8c-3.26 1.85-5.26 5.31-5.24 9.06l-.04 89.79zm14.63-31.54 34.65-20.01 34.65 20v40.01l-34.65 20-34.65-20z'/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M16 8.016A8.522 8.522 0 008.016 16h-.032A8.521 8.521 0 000 8.016v-.032A8.521 8.521 0 007.984 0h.032A8.522 8.522 0 0016 7.984v.032z" fill="url(#prefix__paint0_radial_980_20147)"/><defs><radialGradient id="prefix__paint0_radial_980_20147" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(16.1326 5.4553 -43.70045 129.2322 1.588 6.503)"><stop offset=".067" stop-color="#9168C0"/><stop offset=".343" stop-color="#5684D1"/><stop offset=".672" stop-color="#1BA1E3"/></radialGradient></defs></svg>

After

Width:  |  Height:  |  Size: 599 B

View File

@ -0,0 +1,5 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 24C0 12.6863 0 7.02944 3.51472 3.51472C7.02944 0 12.6863 0 24 0C35.3137 0 40.9706 0 44.4853 3.51472C48 7.02944 48 12.6863 48 24C48 35.3137 48 40.9706 44.4853 44.4853C40.9706 48 35.3137 48 24 48C12.6863 48 7.02944 48 3.51472 44.4853C0 40.9706 0 35.3137 0 24Z" fill="#B0D7E4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.9414 10.4742C29.524 9.91552 28.1547 10.2942 27.1854 11.1569C26.2387 11.9969 25.6667 13.2929 25.6667 14.6662V19.9995C25.6667 21.2889 26.712 22.3329 28 22.3329H33.3334C34.7067 22.3329 36.0027 21.7595 36.844 20.8142C37.7054 19.8449 38.084 18.4755 37.524 17.0582C36.9355 15.5689 36.0476 14.2161 34.9154 13.0837C33.7831 11.9512 32.4306 11.063 30.9414 10.4742ZM27.6667 19.9995V14.6662C27.6667 13.8302 28.02 13.0902 28.5147 12.6502C28.9854 12.2315 29.5694 12.0835 30.208 12.3355C31.4421 12.8237 32.563 13.5597 33.5014 14.4981C34.4399 15.4365 35.1759 16.5574 35.664 17.7915C35.916 18.4302 35.768 19.0142 35.3494 19.4849C34.9094 19.9795 34.1694 20.3329 33.3334 20.3329H28C27.9116 20.3329 27.8268 20.2977 27.7643 20.2352C27.7018 20.1727 27.6667 20.0879 27.6667 19.9995Z" fill="#007EA7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.66 11.8261C21.8467 11.2981 21.06 11.1461 20.1934 11.2981C19.46 11.4261 18.6734 11.7781 17.8374 12.1528L17.748 12.1928C15.7268 13.098 13.9559 14.4809 12.5877 16.2223C11.2195 17.9638 10.295 20.0116 9.89376 22.1896C9.49254 24.3676 9.62661 26.6105 10.2845 28.7252C10.9423 30.8399 12.1042 32.763 13.6701 34.3291C15.2361 35.8951 17.1591 37.0571 19.2737 37.7151C21.3884 38.3731 23.6312 38.5074 25.8093 38.1063C27.9873 37.7052 30.0352 36.7809 31.7768 35.4128C33.5183 34.0447 34.9013 32.2739 35.8067 30.2528L35.8467 30.1621C36.2214 29.3261 36.5734 28.5394 36.7014 27.8048C36.852 26.9408 36.7014 26.1528 36.1734 25.3381C35.6054 24.4621 34.8267 24.0328 33.8747 23.8381C33.0294 23.6648 31.9734 23.6648 30.7627 23.6661H28.6667C27.3814 23.6661 26.5174 23.6634 25.8734 23.5781C25.2587 23.4941 24.9947 23.3514 24.8214 23.1781C24.648 23.0048 24.5054 22.7408 24.4214 22.1248C24.336 21.4821 24.3334 20.6181 24.3334 19.3328V17.2368C24.3334 16.0261 24.3334 14.9701 24.16 14.1248C23.9667 13.1728 23.5374 12.3941 22.66 11.8261ZM18.5654 14.0181C19.524 13.5888 20.08 13.3474 20.5387 13.2674C20.892 13.2061 21.1707 13.2434 21.5734 13.5048C21.912 13.7248 22.092 13.9861 22.2014 14.5248C22.328 15.1474 22.3334 15.9968 22.3334 17.3328V19.4021C22.3334 20.5994 22.3334 21.5994 22.44 22.3914C22.552 23.2288 22.8 23.9848 23.408 24.5914C24.0147 25.1994 24.7707 25.4474 25.608 25.5594C26.4 25.6661 27.4 25.6661 28.5974 25.6661H30.6667C32.0027 25.6661 32.852 25.6714 33.4747 25.7981C34.0134 25.9074 34.2747 26.0874 34.4947 26.4261C34.756 26.8288 34.7934 27.1074 34.732 27.4621C34.652 27.9194 34.4107 28.4754 33.9814 29.4354C32.762 32.154 30.5497 34.3037 27.7974 35.4448C24.9653 36.617 21.7856 36.6277 18.9457 35.4744C16.1058 34.3211 13.8339 32.0965 12.621 29.2815C11.4081 26.4666 11.3518 23.2874 12.4641 20.4312C13.5765 17.575 15.7681 15.2713 18.5654 14.0181Z" fill="#007EA7"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9999 17C11.7447 16.9987 11.4929 16.9406 11.2628 16.8299C11.0328 16.7192 10.8302 16.5587 10.6699 16.36L6.45995 11.26C6.21394 10.953 6.05913 10.583 6.01316 10.1923C5.9672 9.80153 6.03191 9.40574 6.19995 9.05C6.33623 8.74083 6.55862 8.47741 6.84057 8.29122C7.12251 8.10503 7.45209 8.00393 7.78995 8H16.2099C16.5478 8.00393 16.8774 8.10503 17.1593 8.29122C17.4413 8.47741 17.6637 8.74083 17.7999 9.05C17.968 9.40574 18.0327 9.80153 17.9867 10.1923C17.9408 10.583 17.786 10.953 17.5399 11.26L13.3299 16.36C13.1696 16.5587 12.9671 16.7192 12.7371 16.8299C12.507 16.9406 12.2552 16.9987 11.9999 17Z" fill="#007EA7"/>
</svg>

After

Width:  |  Height:  |  Size: 726 B

View File

@ -0,0 +1,5 @@
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="56" height="56" rx="28" fill="#195D80"/>
<path d="M33.8001 18.0995L23.8701 20.5895C22.4201 20.9495 20.9501 22.4195 20.5901 23.8695L18.1001 33.7995C17.3501 36.7995 19.1901 38.6495 22.2001 37.8995L32.1301 35.4195C33.5701 35.0595 35.0501 33.5795 35.4101 32.1395L37.9001 22.1995C38.6501 19.1995 36.8001 17.3495 33.8001 18.0995Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M28 31.5C29.933 31.5 31.5 29.933 31.5 28C31.5 26.067 29.933 24.5 28 24.5C26.067 24.5 24.5 26.067 24.5 28C24.5 29.933 26.067 31.5 28 31.5Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 751 B

View File

@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Grok</title><path d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path></svg>

After

Width:  |  Height:  |  Size: 756 B

View File

@ -0,0 +1,5 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 24C0 12.6863 0 7.02944 3.51472 3.51472C7.02944 0 12.6863 0 24 0C35.3137 0 40.9706 0 44.4853 3.51472C48 7.02944 48 12.6863 48 24C48 35.3137 48 40.9706 44.4853 44.4853C40.9706 48 35.3137 48 24 48C12.6863 48 7.02944 48 3.51472 44.4853C0 40.9706 0 35.3137 0 24Z" fill="#B0D7E4"/>
<path d="M28.6667 15.3337C28.6667 12.7563 26.5774 10.667 24 10.667C21.4227 10.667 19.3334 12.7563 19.3334 15.3337V24.0003C19.3334 26.5777 21.4227 28.667 24 28.667C26.5774 28.667 28.6667 26.5777 28.6667 24.0003V15.3337Z" stroke="#007EA7" stroke-width="2" stroke-linejoin="round"/>
<path d="M14 23.334C14 28.8567 18.4773 33.334 24 33.334M24 33.334C29.5227 33.334 34 28.8567 34 23.334M24 33.334V37.334" stroke="#007EA7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@ -178,10 +178,7 @@ class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
providers: [
ChangeNotifierProvider<PodcastsState>(create: (context) => PodcastsState()),
ChangeNotifierProvider<MediaProvider>(create: (context) => MediaProvider()),
// --- MODIFIED ---
// حذف ..fetchWelcomeMessage()
ChangeNotifierProvider<UserProvider>(create: (context) => UserProvider()),
// --- END MODIFIED ---
ChangeNotifierProvider<ThemeProvider>(create: (context) => ThemeProvider()),
ChangeNotifierProvider<StudioDetailsState>(create: (context) => StudioDetailsState()),
ChangeNotifierProvider<HistoryAiChatState>(create: (context) => HistoryAiChatState()),

View File

@ -1,5 +1,3 @@
// lib/models/ai/ai_model_enum.dart
enum AiModel {
chatGPT,
gemini,

View File

@ -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<MapEntry> _statisticMarkQueue = [];
static final List<Map> _itemMarkQueue = [];
// --- ADDED ---
Future<void> 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<String?> setAndGetToken({String? newToken}) async {
try {
@ -103,25 +102,25 @@ class UserProvider extends CoreProvier {
}
Future<bool> 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<void> _registerFirebaseToken() async {
@ -373,4 +366,4 @@ class UserProvider extends CoreProvier {
//     _unreadMessageCount = service.result['unread'] ?? 0;
//   }
// }
}
}

View File

@ -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}) =>

File diff suppressed because it is too large Load Diff

View File

@ -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<String> suggestedQuestions = [];
bool isLoadingQuestions = true;
// --- END ADDED ---
Future<void> _scrolledEnd() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
@ -80,23 +76,18 @@ class AiChatState extends CoreProvier {
return service;
}
// --- ADDED ---
Future<void> 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<dynamic> 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<void> 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));
}
}
}

View File

@ -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<bool>? onSearchModeToggled;
final bool showModelSelector;
final AiModel selectedModel;
final ValueChanged<AiModel>? 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<AiMessageBar> {
@override
Widget build(BuildContext context) {
return Consumer<AiChatState>(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<AiMessageBar> {
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<AiMessageBar> {
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<AiMessageBar> {
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<AiMessageBar> {
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<AiMessageBar> {
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<AiMessageBar> {
],
),
),
const PopupMenuItem<AiModel>(
PopupMenuItem<AiModel>(
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<AiModel>(
PopupMenuItem<AiModel>(
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<AiMessageBar> {
children: [
SvgPicture.asset(
icon,
height: 20,
height: 17,
),
const SizedBox(width: 8.0),
DidvanText(
@ -432,30 +421,26 @@ class _AiMessageBarState extends State<AiMessageBar> {
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<AiMessageBar> {
)
],
),
// --- 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<AiMessageBar> {
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<AiMessageBar> {
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<AiMessageBar> {
),
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<AiMessageBar> {
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<AiMessageBar> {
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<AiMessageBar> {
));
}
// --- MODIFIED BLOCK: کد صحیح تابع ---
Widget audioContainer() {
final state = context.watch<AiChatState>();
@ -937,12 +911,11 @@ class _AiMessageBarState extends State<AiMessageBar> {
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<AiChatState>();

View File

@ -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,
),
);
}

View File

@ -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 {
),
);
}
}
}

View File

@ -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<AiSectionPage> {
class _AiSectionPageState extends State<AiSectionPage> with TickerProviderStateMixin {
late final List<_AiSectionGridItem> _gridItems;
late AnimationController _fadeController;
late AnimationController _slideController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _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<Offset>(
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<HistoryAiChatState>();
if (historyAiChatState.bots.isEmpty) {
@ -61,7 +91,7 @@ class _AiSectionPageState extends State<AiSectionPage> {
_gridItems = [
_AiSectionGridItem(
title: 'ساخت عکس',
description: 'ایجاد تصاویر خلاقانه با هوش مصنوعی', // توضیحات اضافه شد
description: 'ایجاد تصاویر خلاقانه با هوش مصنوعی',
iconPath: 'lib/assets/icons/create image.svg',
onTap: (context) {
final aiState = context.read<AiState>();
@ -80,7 +110,7 @@ class _AiSectionPageState extends State<AiSectionPage> {
),
_AiSectionGridItem(
title: 'ترجمه',
description: 'ترجمه متون به زبان‌های مختلف', // توضیحات اضافه شد
description: 'ترجمه متون به زبان‌های مختلف',
iconPath: 'lib/assets/icons/translate.svg',
onTap: (context) {
final aiState = context.read<AiState>();
@ -99,13 +129,12 @@ class _AiSectionPageState extends State<AiSectionPage> {
),
_AiSectionGridItem(
title: 'خلاصه‌ساز',
description: 'ساخت نمودار با تحلیل هوشمند',
description: 'خلاصه‌سازی متن با هوش مصنوعی',
iconPath: 'lib/assets/icons/summary.svg',
onTap: (context) {
final aiState = context.read<AiState>();
aiState.endChat();
// ساخت BotsModel برای Aisummery
final aisummeryBot = BotsModel(
id: 100,
name: 'Aisummery',
@ -117,9 +146,54 @@ class _AiSectionPageState extends State<AiSectionPage> {
aiState.startChat(AiChatArgs(bot: aisummeryBot));
},
),
_AiSectionGridItem(
title: 'متن به صوت',
description: 'تبدیل متن به فایل صوتی',
iconPath: 'lib/assets/icons/text to voice.svg',
onTap: (context) {
final aiState = context.read<AiState>();
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>();
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<AiState>();
@ -136,23 +210,32 @@ class _AiSectionPageState extends State<AiSectionPage> {
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<AiSectionPage> {
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<double>(
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<double>(
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<AiSectionPage> {
}
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<double>(
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<double>(
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,
),
),
],
),
),
),
),
);

View File

@ -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<Widget> 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<SwotItem> 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 {
),
);
}
}
}

View File

@ -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);

View File

@ -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 {
),
);
}
}
}

View File

@ -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,

View File

@ -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<HoshanHomeAppBar> {
// --- REMOVED ---
// String? _welcomeMessage;
// bool _isLoadingWelcome = true;
@ -30,7 +29,6 @@ class _HoshanHomeAppBarState extends State<HoshanHomeAppBar> {
// }
// Future<void> fetchWelcomeMessage() async { ... }
// --- END REMOVED ---
void showHistoryDrawer(BuildContext context) {
showGeneralDialog(
@ -82,9 +80,7 @@ class _HoshanHomeAppBarState extends State<HoshanHomeAppBar> {
@override
Widget build(BuildContext context) {
// --- ADDED ---
final userProvider = context.watch<UserProvider>();
// --- END ADDED ---
return Container(
decoration: const BoxDecoration(
@ -132,7 +128,6 @@ class _HoshanHomeAppBarState extends State<HoshanHomeAppBar> {
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<HoshanHomeAppBar> {
color: Theme.of(context).colorScheme.title,
)
: const SizedBox(height: 20),
// --- END MODIFIED ---
),
GestureDetector(
onTap: () {
@ -232,7 +226,7 @@ class _HoshanHomeAppBarState extends State<HoshanHomeAppBar> {
}
class HistoryDrawerContent extends StatefulWidget {
const HistoryDrawerContent();
const HistoryDrawerContent({super.key});
@override
State<HistoryDrawerContent> createState() => _HistoryDrawerContentState();
@ -314,6 +308,7 @@ class _HistoryDrawerContentState extends State<HistoryDrawerContent> {
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<HistoryDrawerContent> {
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<HistoryDrawerContent> {
),
);
}
}
}