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

815 lines
33 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.

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/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: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> {
// 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,
);
},
);
}
void _startNewChat(BuildContext context) {
final historyState = context.read<HistoryAiChatState>();
void navigateToNewChat() {
if (historyState.bots.isNotEmpty) {
Navigator.of(context).pushNamed(
Routes.aiChat,
arguments: AiChatArgs(
bot: historyState.bots.first,
isTool: historyState.bots,
),
);
}
}
if (historyState.bots.isEmpty) {
historyState.getBots();
Future.delayed(const Duration(milliseconds: 500), navigateToNewChat);
} else {
navigateToNewChat();
}
}
@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: () {
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);
return Align(
alignment: Alignment.centerLeft,
child: Material(
color: Colors.transparent,
child: Container(
width: MediaQuery.of(context).size.width * 0.75,
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(
// ignore: deprecated_member_use
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,
// ignore: deprecated_member_use
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('ذخیره'),
),
],
),
);
}
}