diff --git a/lib/ui/ai_assistant/assistant_sidebar.dart b/lib/ui/ai_assistant/assistant_sidebar.dart index 8eb8aeac4..e4e2afc8c 100644 --- a/lib/ui/ai_assistant/assistant_sidebar.dart +++ b/lib/ui/ai_assistant/assistant_sidebar.dart @@ -2,8 +2,9 @@ import 'package:campus_mobile_experimental/app_styles.dart'; import 'package:campus_mobile_experimental/core/models/tgpt_models/chat_history.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -class AssistantSidebar extends StatelessWidget { +class AssistantSidebar extends StatefulWidget { const AssistantSidebar({ super.key, required this.isLoggedIn, @@ -21,14 +22,61 @@ class AssistantSidebar extends StatelessWidget { final Future Function(String sessionId) onSelectSession; final VoidCallback onClose; + @override + State createState() => _AssistantSidebarState(); +} + +class _AssistantSidebarState extends State { + static const String _previous7DaysExpandedKey = 'tgpt_sidebar_previous_7_days_expanded'; + static const String _olderExpandedKey = 'tgpt_sidebar_older_expanded'; + + bool _isPrevious7DaysExpanded = true; + bool _isOlderExpanded = true; + + @override + void initState() { + super.initState(); + _loadExpandedPreferences(); + } + + Future _loadExpandedPreferences() async { + final SharedPreferences preferences = await SharedPreferences.getInstance(); + if (!mounted) { + return; + } + + setState(() { + _isPrevious7DaysExpanded = preferences.getBool(_previous7DaysExpandedKey) ?? true; + _isOlderExpanded = preferences.getBool(_olderExpandedKey) ?? true; + }); + } + + Future _setPrevious7DaysExpanded(bool isExpanded) async { + setState(() { + _isPrevious7DaysExpanded = isExpanded; + }); + + final SharedPreferences preferences = await SharedPreferences.getInstance(); + await preferences.setBool(_previous7DaysExpandedKey, isExpanded); + } + + Future _setOlderExpanded(bool isExpanded) async { + setState(() { + _isOlderExpanded = isExpanded; + }); + + final SharedPreferences preferences = await SharedPreferences.getInstance(); + await preferences.setBool(_olderExpandedKey, isExpanded); + } + @override Widget build(BuildContext context) { final double drawerWidth = MediaQuery.sizeOf(context).width * 0.78; final DateTime cutoff = DateTime.now().subtract(const Duration(days: 7)); final List recentSessions = - sessions.where((ChatSessionMeta session) => session.updatedAt.isAfter(cutoff)).toList(); + widget.sessions.where((ChatSessionMeta session) => session.updatedAt.isAfter(cutoff)).toList(); final List olderSessions = - sessions.where((ChatSessionMeta session) => !session.updatedAt.isAfter(cutoff)).toList(); + widget.sessions.where((ChatSessionMeta session) => !session.updatedAt.isAfter(cutoff)).toList(); return Drawer( width: drawerWidth, @@ -45,7 +93,7 @@ class AssistantSidebar extends StatelessWidget { children: [ const Spacer(), IconButton( - onPressed: onClose, + onPressed: widget.onClose, splashRadius: 20, icon: SvgPicture.asset( 'assets/images/tgpt/pin-sidebar.svg', @@ -59,7 +107,7 @@ class AssistantSidebar extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 14), child: InkWell( - onTap: onNewChat, + onTap: widget.onNewChat, borderRadius: BorderRadius.circular(14), child: Ink( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), @@ -108,18 +156,26 @@ class AssistantSidebar extends StatelessWidget { child: ListView( padding: const EdgeInsets.only(bottom: 24), children: [ - if (isLoggedIn) ...[ + if (widget.isLoggedIn) ...[ if (recentSessions.isNotEmpty) ...[ - const _SidebarSectionTitle(title: 'Previous 7 Days'), - ...recentSessions.map(_buildSessionRow), + _SidebarSectionTitle( + title: 'Previous 7 Days', + isExpanded: _isPrevious7DaysExpanded, + onTap: () => _setPrevious7DaysExpanded(!_isPrevious7DaysExpanded), + ), + if (_isPrevious7DaysExpanded) ...recentSessions.map(_buildSessionRow), ], if (olderSessions.isNotEmpty) ...[ const SizedBox(height: 12), - const _SidebarSectionTitle(title: 'Older'), - ...olderSessions.map(_buildSessionRow), + _SidebarSectionTitle( + title: 'Older', + isExpanded: _isOlderExpanded, + onTap: () => _setOlderExpanded(!_isOlderExpanded), + ), + if (_isOlderExpanded) ...olderSessions.map(_buildSessionRow), ], ] else - ...sessions.map(_buildSessionRow), + ...widget.sessions.map(_buildSessionRow), ], ), ), @@ -130,12 +186,12 @@ class AssistantSidebar extends StatelessWidget { } Widget _buildSessionRow(ChatSessionMeta session) { - final bool isActive = session.id == activeSessionId; + final bool isActive = session.id == widget.activeSessionId; return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 1), child: InkWell( - onTap: () => onSelectSession(session.id), + onTap: () => widget.onSelectSession(session.id), borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 11), @@ -161,31 +217,44 @@ class AssistantSidebar extends StatelessWidget { } class _SidebarSectionTitle extends StatelessWidget { - const _SidebarSectionTitle({required this.title}); + const _SidebarSectionTitle({ + required this.title, + required this.isExpanded, + required this.onTap, + }); final String title; + final bool isExpanded; + final VoidCallback onTap; @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.fromLTRB(12, 2, 16, 4), - child: Row( - children: [ - const Icon( - Icons.arrow_drop_down, - size: 26, - color: lightPrimaryColor, - ), - Text( - title, - style: const TextStyle( - fontFamily: 'Brix Sans', - fontSize: 15, - fontWeight: FontWeight.w700, - color: lightPrimaryColor, - ), + padding: const EdgeInsets.fromLTRB(8, 0, 16, 0), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 8, 0, 8), + child: Row( + children: [ + Icon( + isExpanded ? Icons.arrow_drop_down : Icons.arrow_right, + size: 26, + color: lightPrimaryColor, + ), + Text( + title, + style: const TextStyle( + fontFamily: 'Brix Sans', + fontSize: 15, + fontWeight: FontWeight.w700, + color: lightPrimaryColor, + ), + ), + ], ), - ], + ), ), ); } diff --git a/test/ui/ai_assistant/assistant_sidebar_test.dart b/test/ui/ai_assistant/assistant_sidebar_test.dart index 4c8900d66..c3bb128e5 100644 --- a/test/ui/ai_assistant/assistant_sidebar_test.dart +++ b/test/ui/ai_assistant/assistant_sidebar_test.dart @@ -2,8 +2,11 @@ import 'package:campus_mobile_experimental/core/models/tgpt_models/chat_history. import 'package:campus_mobile_experimental/ui/ai_assistant/assistant_sidebar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { + const String previous7DaysExpandedKey = 'tgpt_sidebar_previous_7_days_expanded'; + const String olderExpandedKey = 'tgpt_sidebar_older_expanded'; final DateTime now = DateTime.now(); final List sessions = [ ChatSessionMeta( @@ -33,8 +36,17 @@ void main() { ); } + Future pumpSidebar(WidgetTester tester, {required bool isLoggedIn}) async { + await tester.pumpWidget(buildSidebar(isLoggedIn: isLoggedIn)); + await tester.pumpAndSettle(); + } + + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + testWidgets('shows time buckets for signed-in users', (WidgetTester tester) async { - await tester.pumpWidget(buildSidebar(isLoggedIn: true)); + await pumpSidebar(tester, isLoggedIn: true); expect(find.text('Previous 7 Days'), findsOneWidget); expect(find.text('Older'), findsOneWidget); @@ -43,11 +55,72 @@ void main() { }); testWidgets('shows a flat chat list for guests', (WidgetTester tester) async { - await tester.pumpWidget(buildSidebar(isLoggedIn: false)); + await pumpSidebar(tester, isLoggedIn: false); expect(find.text('Previous 7 Days'), findsNothing); expect(find.text('Older'), findsNothing); expect(find.text('Recent Chat'), findsOneWidget); expect(find.text('Older Chat'), findsOneWidget); }); + + testWidgets('collapses and expands previous 7 days section for signed-in users', (WidgetTester tester) async { + await pumpSidebar(tester, isLoggedIn: true); + + await tester.tap(find.text('Previous 7 Days')); + await tester.pumpAndSettle(); + + expect(find.text('Previous 7 Days'), findsOneWidget); + expect(find.text('Recent Chat'), findsNothing); + expect(find.text('Older Chat'), findsOneWidget); + + final SharedPreferences preferences = await SharedPreferences.getInstance(); + expect(preferences.getBool(previous7DaysExpandedKey), isFalse); + + await tester.tap(find.text('Previous 7 Days')); + await tester.pumpAndSettle(); + + expect(find.text('Recent Chat'), findsOneWidget); + expect(preferences.getBool(previous7DaysExpandedKey), isTrue); + }); + + testWidgets('collapses and expands older section for signed-in users', (WidgetTester tester) async { + await pumpSidebar(tester, isLoggedIn: true); + + await tester.tap(find.text('Older')); + await tester.pumpAndSettle(); + + expect(find.text('Older'), findsOneWidget); + expect(find.text('Older Chat'), findsNothing); + expect(find.text('Recent Chat'), findsOneWidget); + + final SharedPreferences preferences = await SharedPreferences.getInstance(); + expect(preferences.getBool(olderExpandedKey), isFalse); + + await tester.tap(find.text('Older')); + await tester.pumpAndSettle(); + + expect(find.text('Older Chat'), findsOneWidget); + expect(preferences.getBool(olderExpandedKey), isTrue); + }); + + testWidgets('loads saved section expansion preferences', (WidgetTester tester) async { + SharedPreferences.setMockInitialValues({ + previous7DaysExpandedKey: false, + olderExpandedKey: true, + }); + + await pumpSidebar(tester, isLoggedIn: true); + + expect(find.text('Previous 7 Days'), findsOneWidget); + expect(find.text('Older'), findsOneWidget); + expect(find.text('Recent Chat'), findsNothing); + expect(find.text('Older Chat'), findsOneWidget); + + await tester.tap(find.text('Previous 7 Days')); + await tester.pumpAndSettle(); + + final SharedPreferences preferences = await SharedPreferences.getInstance(); + expect(preferences.getBool(previous7DaysExpandedKey), isTrue); + expect(find.text('Recent Chat'), findsOneWidget); + }); }