import "package:go_router/go_router.dart"; import "package:provider/provider.dart"; import "package:shadcn_flutter/shadcn_flutter.dart"; import "../../../src/project_store.dart"; import "../../providers/chat_provider.dart"; import "../../providers/home_coordinator.dart"; import "../../providers/projects_provider.dart"; import "../../widgets/chat/chat_box.dart"; import "../../widgets/chat/chat_view.dart"; import "../../widgets/common/footer_bar.dart"; import "../../widgets/common/app_header.dart"; import "../../widgets/common/button.dart"; import "../../widgets/sidebar/sidebar.dart"; import "../../widgets/sidebar/sidebar_v2.dart"; class NewHomeScreen extends StatefulWidget { const NewHomeScreen({super.key}); @override State createState() => _NewHomeScreenState(); } Color _centerBgColor(BuildContext context) { final theme = Theme.of(context); final dark = theme.brightness == Brightness.dark; final h = HSLColor.fromColor(theme.colorScheme.border).hue; return dark ? HSLColor.fromAHSL(1, h, 0.35, 0.13).toColor() : HSLColor.fromAHSL(1, h, 0.30, 0.88).toColor(); } class _NewHomeScreenState extends State { final ScrollController _chatScrollController = ScrollController(); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().addListener(_onCoordinatorChanged); }); } @override void dispose() { context.read().removeListener(_onCoordinatorChanged); _chatScrollController.dispose(); super.dispose(); } void _onCoordinatorChanged() { final coordinator = context.read(); final err = coordinator.error; if (err != null) { coordinator.clearError(); _showError(err); } } Future _showError(String message) { return showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text("Heads up"), content: Text(message), actions: [ Button.primary( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text("OK"), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( child: Column( children: [ AppHeader(), Expanded( child: ColoredBox( color: _centerBgColor(context), child: Stack( children: [ _ChatArea(scrollController: _chatScrollController), Positioned( top: 0, bottom: 0, right: 0, width: 12, child: ChatScrollBar(controller: _chatScrollController), ), Positioned.fill( child: IgnorePointer( child: CustomPaint(painter: _InsetShadowPainter()), ), ), Positioned( top: 12, left: 12, bottom: 12, child: _SidebarPane(), ), ], ), ), ), FooterBar(), ], ), ); } } class _ChatArea extends StatelessWidget { final ScrollController scrollController; const _ChatArea({required this.scrollController}); @override Widget build(BuildContext context) { final chatProvider = context.watch(); return Container( alignment: Alignment.center, // padding: const EdgeInsets.all(16), padding: EdgeInsets.only( left: 16, right: 16 ), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 700), child: Column( children: [ if (chatProvider.messages.isEmpty)...[ Expanded( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ _EmptyChatState(), ChatBox(), ], ), ) ), ] else ...[ Expanded(child: ChatView(scrollController: scrollController)), ChatBox(), Gap(12), ], ], ), ), ); } } class _EmptyChatState extends StatelessWidget { const _EmptyChatState(); @override Widget build(BuildContext context) { final projectsProvider = context.watch(); final projects = projectsProvider.projects; final selected = projectsProvider.selectedProject; final coordinator = context.read(); return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Lets burn some braincells", textAlign: TextAlign.center, ).x4Large.extraBold, Text( "Select a project and thread from the sidebar, or start a new chat.", textAlign: TextAlign.center, ).textSmall.muted, ], ), ), ); } } class _InsetShadowPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { const blur = 12.0; final rect = Offset.zero & size; canvas.save(); canvas.clipRect(rect); // restrict to outer ring so shadow doesnt bleed into center final innerRect = rect.deflate(24); final ringClip = Path() ..addRect(rect) ..addRect(innerRect) ..fillType = PathFillType.evenOdd; canvas.clipPath(ringClip); final path = Path() ..addRect(rect.inflate(blur * 2)) ..addRect(rect) ..fillType = PathFillType.evenOdd; final paint = Paint() ..color = const Color(0x55000000) ..maskFilter = const MaskFilter.blur(BlurStyle.normal, blur); canvas.drawPath(path, paint); canvas.restore(); } @override bool shouldRepaint(covariant CustomPainter old) => false; } class _SidebarPane extends StatefulWidget { const _SidebarPane(); @override State<_SidebarPane> createState() => _SidebarPaneState(); } class _SidebarPaneState extends State<_SidebarPane> { bool _open = true; static const _dur = Duration(milliseconds: 220); static const _curve = Curves.easeInOut; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Stack( clipBehavior: Clip.none, children: [ AnimatedSlide( offset: _open ? Offset.zero : const Offset(-1.1, 0), duration: _dur, curve: _curve, child: AnimatedOpacity( opacity: _open ? 1.0 : 0.0, duration: _dur, curve: _curve, child: OutlinedContainer( borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 16, spreadRadius: 2, ), ], child: SidebarV2(onClose: () => setState(() => _open = false)), ), ), ), AnimatedOpacity( opacity: _open ? 0.0 : 1.0, duration: _dur, curve: _curve, child: IgnorePointer( ignoring: _open, child: IconButton.ghost( onPressed: () => setState(() => _open = true), icon: Icon( LucideIcons.panelLeftOpen, size: 16, color: theme.colorScheme.mutedForeground, ), ), ), ), ], ); } } abstract class HomeScreenRoute { static const path = '/'; static const name = 'home'; static GoRoute get route => GoRoute( path: path, name: name, builder: (context, state) => const NewHomeScreen(), ); }