didvan-app/lib/views/widgets/hoshan_home_app_bar.dart

929 lines
37 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ignore_for_file: deprecated_member_use
import 'package:didvan/config/design_config.dart';
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/models/ai/bots_model.dart';
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/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
import 'package:didvan/routes/routes.dart';
// ignore: depend_on_referenced_packages
import 'package:shamsi_date/shamsi_date.dart';
class HoshanHomeAppBar extends StatefulWidget {
const HoshanHomeAppBar({super.key});
@override
State<HoshanHomeAppBar> createState() => _HoshanHomeAppBarState();
}
class _HoshanHomeAppBarState extends State<HoshanHomeAppBar> {
/// مدلی که کاربر برای شروع چت بعدی انتخاب کرده. اگر null باشد، اولین
/// bot لیست (پیش‌فرض = GPT 5.5) استفاده می‌شود.
BotsModel? _selectedBot;
/// منوی انتخاب مدل را به‌صورت BottomSheet نشان می‌دهد و انتخاب کاربر
/// را در state ذخیره می‌کند.
Future<void> _pickBot(BuildContext context) async {
final historyState = context.read<HistoryAiChatState>();
if (historyState.bots.isEmpty) {
await historyState.getBots();
}
if (historyState.bots.isEmpty || !context.mounted) return;
final selected = await showModalBottomSheet<BotsModel>(
context: context,
backgroundColor: Theme.of(context).colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 8),
DidvanText(
'انتخاب مدل هوش مصنوعی',
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.title,
),
const SizedBox(height: 8),
for (final bot in historyState.bots)
ListTile(
leading: CircleAvatar(
radius: 18,
backgroundColor: Colors.transparent,
backgroundImage: (bot.image ?? '').isNotEmpty
? NetworkImage(bot.image!)
: null,
child: (bot.image ?? '').isEmpty
? const Icon(Icons.smart_toy)
: null,
),
title: DidvanText(
bot.short ?? bot.name ?? 'مدل',
color: Theme.of(context).colorScheme.title,
),
subtitle: bot.description != null
? DidvanText(
bot.description!,
fontSize: 12,
color: Theme.of(context).colorScheme.caption,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: null,
trailing: (_selectedBot?.id ?? historyState.bots.first.id) ==
bot.id
? const Icon(Icons.check_circle, color: Colors.green)
: null,
onTap: () => Navigator.of(ctx).pop(bot),
),
const SizedBox(height: 8),
],
),
);
},
);
if (selected != null && mounted) {
setState(() => _selectedBot = selected);
}
}
/// مدل فعلی (انتخاب کاربر یا اولین bot موجود).
BotsModel? _currentBot(HistoryAiChatState state) {
if (_selectedBot != null) return _selectedBot;
if (state.bots.isNotEmpty) return state.bots.first;
return null;
}
// String? _welcomeMessage;
// bool _isLoadingWelcome = true;
// @override
// void initState() {
// super.initState();
// fetchWelcomeMessage();
// }
// Future<void> fetchWelcomeMessage() async { ... }
void showHistoryDrawer(BuildContext context) {
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) {
return const HistoryDrawerContent();
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(-1, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeInOut,
)),
child: child,
);
},
);
}
/// "سوال خود را بپرسید" → ایجاد چت جدید با مدل انتخاب‌شدهٔ کاربر
/// (یا اولین bot لیست به‌عنوان پیش‌فرض).
Future<void> _startNewChat(BuildContext context) async {
final historyState = context.read<HistoryAiChatState>();
final navigator = Navigator.of(context);
if (historyState.bots.isEmpty) {
await historyState.getBots();
}
if (historyState.bots.isEmpty || !context.mounted) return;
final bot = _selectedBot ?? historyState.bots.first;
try {
navigator.pushNamed(
Routes.aiChat,
arguments: AiChatArgs(
bot: bot,
isTool: historyState.bots,
),
);
} catch (_) {}
}
@override
Widget build(BuildContext context) {
final userProvider = context.watch<UserProvider>();
return Container(
decoration: BoxDecoration(
color: DesignConfig.isDark
? const Color.fromARGB(255, 78, 82, 84)
: const Color.fromRGBO(230, 242, 246, 1),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(32),
bottomRight: Radius.circular(32),
),
),
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgPicture.asset(
Assets.horizontalLogoWithText,
height: 60,
color: Theme.of(context).colorScheme.title,
),
const Spacer(),
GestureDetector(
onTap: () {
// ignore: avoid_print
print('history bottom tapped');
final historyState = context.read<HistoryAiChatState>();
if (historyState.chats.isEmpty) {
historyState.getChats();
}
showHistoryDrawer(context);
},
child: Container(
padding: const EdgeInsets.all(8),
child: SvgPicture.asset(
'lib/assets/icons/history.svg',
height: 24,
color: Theme.of(context).colorScheme.inputText,
),
),
),
],
),
Container(
padding: const EdgeInsets.fromLTRB(8, 35, 8, 35),
alignment: Alignment.center,
child: userProvider.isLoadingWelcome
? const SizedBox(height: 20)
: userProvider.welcomeMessage != null
? DidvanText(
userProvider.welcomeMessage!,
fontSize: 21,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.title,
)
: const SizedBox(height: 20),
),
GestureDetector(
onTap: () {
_startNewChat(context);
},
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 11),
decoration: BoxDecoration(
color: DesignConfig.isDark
? const Color.fromARGB(255, 195, 195, 196)
: Colors.white,
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: const Color.fromARGB(255, 25, 93, 128),
width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
GestureDetector(
onTap: () {
_startNewChat(context);
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: const BoxDecoration(
color: Color.fromARGB(255, 25, 93, 128),
shape: BoxShape.circle,
),
child: SvgPicture.asset(
'lib/assets/icons/microphone-2.svg',
width: 20,
height: 20,
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
),
),
),
const SizedBox(width: 12),
const Expanded(
child: DidvanText(
'سوال خود را بپرسید...',
color: Color.fromARGB(255, 18, 17, 16),
fontSize: 14,
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () {
_startNewChat(context);
},
child: Container(
padding: const EdgeInsets.all(1),
child: SvgPicture.asset(
'lib/assets/icons/send_bold.svg',
width: 30,
height: 30,
),
),
),
],
),
),
),
const SizedBox(height: 12),
const DidvanText(
'مدل‌های هوش مصنوعی می‌توانند اشتباه کنند، صحت اطلاعات مهم را بررسی کنید.',
textAlign: TextAlign.center,
fontSize: 11,
color: Color.fromARGB(255, 18, 17, 16),
maxLines: 3,
),
],
),
),
),
);
}
}
class HistoryDrawerContent extends StatefulWidget {
const HistoryDrawerContent({super.key});
@override
State<HistoryDrawerContent> createState() => _HistoryDrawerContentState();
}
class _HistoryDrawerContentState extends State<HistoryDrawerContent> {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
@override
void initState() {
super.initState();
_searchController.addListener(() {
setState(() {
_searchQuery = _searchController.text;
});
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
DateTime? _parseDateString(String? dateString) {
if (dateString == null) return null;
try {
return DateTime.parse(dateString);
} catch (e) {
return null;
}
}
String _getOlderGroupName(DateTime date) {
final now = DateTime.now();
final nowJalali = Jalali.fromDateTime(now);
final dateJalali = Jalali.fromDateTime(date);
if (nowJalali.year != dateJalali.year) {
return dateJalali.year.toString();
}
return _getMonthName(dateJalali.month);
}
String _getMonthName(int month) {
const monthNames = [
'فروردین',
'اردیبهشت',
'خرداد',
'تیر',
'مرداد',
'شهریور',
'مهر',
'آبان',
'آذر',
'دی',
'بهمن',
'اسفند',
];
if (month >= 1 && month <= 12) {
return monthNames[month - 1];
}
return month.toString();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
// Calculate responsive width:
// For web: use min of 400px or 30% of screen width
// For mobile: use 75% of screen width
double drawerWidth;
if (kIsWeb) {
if (screenWidth > 1200) {
// Desktop: fixed width of 400px
drawerWidth = 400;
} else if (screenWidth > 768) {
// Tablet: 40% of screen width
drawerWidth = screenWidth * 0.4;
} else {
// Mobile web: 75% of screen width
drawerWidth = screenWidth * 0.75;
}
} else {
// Native mobile: 75% of screen width
drawerWidth = screenWidth * 0.75;
}
return Align(
alignment: Alignment.centerLeft,
child: Material(
color: Colors.transparent,
child: Container(
width: drawerWidth,
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
color: DesignConfig.isDark
? const Color.fromARGB(255, 166, 166, 166)
: const Color.fromARGB(255, 237, 237, 237),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 20,
offset: const Offset(5, 0),
),
],
),
child: Consumer<HistoryAiChatState>(
builder: (context, historyState, child) {
final filteredChats = historyState.chats.where((chat) {
final title = chat.title?.toLowerCase() ?? 'گفتگو';
final query = _searchQuery.toLowerCase();
return title.contains(query);
}).toList();
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final last7Days = today.subtract(const Duration(days: 7));
final last30Days = today.subtract(const Duration(days: 30));
final List<dynamic> todayChats = [];
final List<dynamic> yesterdayChats = [];
final List<dynamic> last7DaysChats = [];
final List<dynamic> last30DaysChats = [];
final Map<String, List<dynamic>> olderChatsGrouped = {};
for (final chat in filteredChats) {
final chatDate = _parseDateString(chat.updatedAt);
final chatDay = chatDate != null
? DateTime(chatDate.year, chatDate.month, chatDate.day)
: null;
if (chatDay == null) {
const groupName = 'قدیمی‌تر';
if (olderChatsGrouped[groupName] == null) {
olderChatsGrouped[groupName] = [];
}
olderChatsGrouped[groupName]!.add(chat);
} else if (chatDay.isAtSameMomentAs(today)) {
todayChats.add(chat);
} else if (chatDay.isAtSameMomentAs(yesterday)) {
yesterdayChats.add(chat);
} else if (chatDay.isAfter(last7Days)) {
last7DaysChats.add(chat);
} else if (chatDay.isAfter(last30Days)) {
last30DaysChats.add(chat);
} else {
final String groupName = _getOlderGroupName(chatDate!);
if (olderChatsGrouped[groupName] == null) {
olderChatsGrouped[groupName] = [];
}
olderChatsGrouped[groupName]!.add(chat);
}
}
return Column(
children: [
SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const SizedBox(height: 50),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgPicture.asset(
'lib/assets/icons/Online Chat Support.svg',
height: 60,
),
],
),
const SizedBox(height: 26),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 11),
decoration: BoxDecoration(
color: DesignConfig.isDark
? const Color.fromARGB(255, 155, 154, 154)
: const Color.fromARGB(255, 237, 237, 237),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: DesignConfig.isDark
? const Color.fromARGB(
255, 107, 106, 106)
: const Color.fromARGB(
255, 223, 223, 223),
width: 1)),
child: Row(
children: [
SvgPicture.asset(
'lib/assets/icons/search-normal2.svg'),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'جستجو',
hintStyle: TextStyle(
color: DesignConfig.isDark
? const Color.fromARGB(
255, 113, 112, 1112)
: const Color.fromARGB(
255, 161, 160, 160),
fontSize: 14),
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
),
),
if (_searchQuery.isNotEmpty)
GestureDetector(
onTap: () {
_searchController.clear();
},
child: Icon(Icons.close,
color: Colors.grey[600], size: 20),
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 30.0),
child: InkWell(
onTap: () {
Navigator.pop(context);
Navigator.of(context)
.pushNamed(Routes.aiArchivedHistory);
},
child: Row(
children: [
SvgPicture.asset(
'lib/assets/icons/direct-inbox.svg',
color:
const Color.fromARGB(255, 61, 61, 61),
height: 20,
),
const SizedBox(width: 8),
DidvanText(
'گفت‌وگوهای آرشیو شده',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(40, 10, 40, 0),
child: Divider(
height: 3,
color: DesignConfig.isDark
? const Color.fromARGB(255, 113, 112, 1112)
: const Color.fromARGB(255, 161, 160, 160),
),
),
Expanded(
child: historyState.loadinggetAll
? const Center(child: CircularProgressIndicator())
: historyState.chats.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.chat_bubble_outline,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
DidvanText(
'هنوز گفتگویی\nوجود ندارد',
textAlign: TextAlign.center,
color: Colors.grey[600],
),
],
),
)
: filteredChats.isEmpty
? Center(
child: DidvanText(
'موردی یافت نشد',
textAlign: TextAlign.center,
color: Colors.grey[600],
),
)
: ListView(
padding:
const EdgeInsets.symmetric(vertical: 4),
children: [
if (todayChats.isNotEmpty) ...[
_buildSectionHeader('امروز'),
...todayChats.map((chat) =>
_buildChatItem(chat, historyState)),
],
if (yesterdayChats.isNotEmpty) ...[
_buildSectionHeader('دیروز'),
...yesterdayChats.map((chat) =>
_buildChatItem(chat, historyState)),
],
if (last7DaysChats.isNotEmpty) ...[
_buildSectionHeader('۷ روز اخیر'),
...last7DaysChats.map((chat) =>
_buildChatItem(chat, historyState)),
],
if (last30DaysChats.isNotEmpty) ...[
_buildSectionHeader('۳۰ روز اخیر'),
...last30DaysChats.map((chat) =>
_buildChatItem(chat, historyState)),
],
...olderChatsGrouped.keys
.map((groupName) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
_buildSectionHeader(groupName),
...olderChatsGrouped[groupName]!
.map((chat) => _buildChatItem(
chat, historyState)),
],
);
}),
],
),
),
],
);
},
),
),
),
);
}
Widget _buildSectionHeader(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 25, 16),
child: DidvanText(
title,
fontSize: 14,
fontWeight: FontWeight.bold,
color: const Color.fromARGB(255, 0, 126, 167),
),
);
}
Widget _buildChatItem(dynamic chat, dynamic historyState) {
final int index = historyState.chats.indexOf(chat);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
child: InkWell(
onTap: () {
Navigator.pop(context);
if (historyState.bots.isNotEmpty) {
Navigator.of(context).pushNamed(
Routes.aiChat,
arguments: AiChatArgs(
chat: chat,
bot: historyState.bots.first,
),
);
}
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 10, 10),
child: SizedBox(
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
chat.title ?? 'گفتگو',
maxLines: 2,
overflow: TextOverflow.ellipsis,
fontSize: 14,
fontWeight: FontWeight.w500,
),
if (chat.updatedAt != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
DidvanText(
_formatDateFromString(chat.updatedAt)
.toPersianDigit(),
fontSize: 12,
color: Colors.grey[600],
),
PopupMenuButton<String>(
icon:
SvgPicture.asset('lib/assets/icons/more.svg'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
elevation: 4,
padding: EdgeInsets.zero,
offset: const Offset(0, 10),
onSelected: (value) async {
if (index == -1) return;
switch (value) {
case 'rename':
_showRenameDialog(
chat, historyState, index);
break;
case 'archive':
await historyState.archivedChat(
chat.id!, index,
refresh: false);
historyState.update();
break;
case 'delete':
await historyState.deleteChat(
chat.id!, index,
refresh: false);
historyState.update();
break;
}
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'rename',
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
child: SizedBox(
width: 180,
child: Column(
children: [
Row(
children: [
SvgPicture.asset(
'lib/assets/icons/edit-2.svg',
height: 20,
),
const SizedBox(width: 16),
const Text('تغییر نام',
style: TextStyle(fontSize: 13)),
],
),
const SizedBox(height: 20),
const Divider(
height: 1,
color: Color.fromARGB(
255, 227, 226, 225),
)
],
),
),
),
PopupMenuItem<String>(
value: 'archive',
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
child: SizedBox(
width: 180,
child: Column(
children: [
Row(
children: [
SvgPicture.asset(
'lib/assets/icons/direct-inbox.svg',
height: 20,
),
const SizedBox(width: 16),
const Text('آرشیو',
style: TextStyle(fontSize: 13)),
],
),
const SizedBox(height: 20),
const Divider(
height: 1,
color: Color.fromARGB(
255, 227, 226, 225),
)
],
),
),
),
PopupMenuItem<String>(
value: 'delete',
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 1),
child: SizedBox(
width: 150,
child: Row(
children: [
SvgPicture.asset(
'lib/assets/icons/trash.svg',
height: 20,
color: const Color.fromARGB(
255, 0, 126, 167),
),
const SizedBox(width: 16),
const Text('حذف',
style: TextStyle(
fontSize: 13,
)),
],
),
),
),
],
),
],
),
],
],
),
),
],
),
),
),
),
);
}
String _formatDate(DateTime date) {
final dateJalali = Jalali.fromDateTime(date);
final year = dateJalali.year.toString();
final month = dateJalali.month.toString().padLeft(2, '0');
final day = dateJalali.day.toString().padLeft(2, '0');
final hour = date.hour.toString().padLeft(2, '0');
final minute = date.minute.toString().padLeft(2, '0');
return '$year.$month.$day - $hour:$minute';
}
String _formatDateFromString(String? dateString) {
if (dateString == null) return '';
try {
final date = DateTime.parse(dateString);
return _formatDate(date);
} catch (e) {
return '';
}
}
void _showRenameDialog(
dynamic chat, HistoryAiChatState historyState, int index) {
final controller = TextEditingController(text: chat.title);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('تغییر نام گفتگو'),
content: TextField(
controller: controller,
decoration: const InputDecoration(
hintText: 'نام جدید را وارد کنید',
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('لغو'),
),
TextButton(
onPressed: () async {
if (controller.text.isNotEmpty) {
await historyState.changeNameChat(
chat.id!,
index,
controller.text,
refresh: false,
);
historyState.update();
}
Navigator.pop(context);
},
child: const Text('ذخیره'),
),
],
),
);
}
}