The-Agency/lib/ui/pages/home_screen/page.dart

307 lines
8 KiB
Dart

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/sidebar/sidebar.dart";
import "../../widgets/sidebar/sidebar_v2.dart";
class NewHomeScreen extends StatefulWidget {
const NewHomeScreen({super.key});
@override
State<NewHomeScreen> 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<NewHomeScreen> {
final ScrollController _chatScrollController = ScrollController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<HomeCoordinator>().addListener(_onCoordinatorChanged);
});
}
@override
void dispose() {
context.read<HomeCoordinator>().removeListener(_onCoordinatorChanged);
_chatScrollController.dispose();
super.dispose();
}
void _onCoordinatorChanged() {
final coordinator = context.read<HomeCoordinator>();
final err = coordinator.error;
if (err != null) {
coordinator.clearError();
_showError(err);
}
}
Future<void> _showError(String message) {
return showDialog<void>(
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<ChatProvider>();
return Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(16),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(
children: [
Expanded(
child: chatProvider.messages.isEmpty
? _EmptyChatState()
: ChatView(scrollController: scrollController),
),
ChatBox(),
],
),
),
);
}
}
class _EmptyChatState extends StatelessWidget {
const _EmptyChatState();
@override
Widget build(BuildContext context) {
final projectsProvider = context.watch<ProjectsProvider>();
final projects = projectsProvider.projects;
final selected = projectsProvider.selectedProject;
final coordinator = context.read<HomeCoordinator>();
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(LucideIcons.messagesSquare, size: 28),
const Gap(16),
Text(
"Ask the agency anything",
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
textAlign: TextAlign.center,
),
const Gap(8),
Text(
"Select a project and thread from the sidebar, or start a new chat.",
textAlign: TextAlign.center,
).textSmall.muted,
const Gap(24),
Select<ProjectRecord>(
itemBuilder: (context, item) => Text(item.name),
popup: SelectPopup.builder(
searchPlaceholder: const Text("Search projects"),
builder: (context, searchQuery) {
final filtered = searchQuery == null || searchQuery.isEmpty
? projects
: projects.where((p) =>
p.name.toLowerCase().contains(searchQuery.toLowerCase()) ||
p.workingDirectory.toLowerCase().contains(searchQuery.toLowerCase())
).toList();
return SelectItemList(
children: [
for (final project in filtered)
SelectItemButton(
value: project,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(project.name),
Text(project.workingDirectory).textSmall.muted,
],
),
),
],
);
},
),
onChanged: (project) {
if (project != null) coordinator.selectProject(project);
},
constraints: const BoxConstraints(minWidth: 240),
value: selected,
placeholder: const Text("Select a project"),
),
],
),
),
);
}
}
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 StatelessWidget {
const _SidebarPane();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return OutlinedContainer(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 16,
spreadRadius: 2,
),
],
child: SidebarV2(),
);
}
}
abstract class HomeScreenRoute {
static const path = '/';
static const name = 'home';
static GoRoute get route => GoRoute(
path: path,
name: name,
builder: (context, state) => const NewHomeScreen(),
);
}