307 lines
8 KiB
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(),
|
|
);
|
|
}
|