Multi-agent AI laboratory with ASP.NET Core 8.0 backend and Flutter frontend. Implements CQRS architecture, OpenAPI contract-first API design. BACKEND: Agent management, conversations, executions with PostgreSQL + Ollama FRONTEND: Cross-platform UI with strict typing and Result-based error handling Co-Authored-By: Jean-Philippe Brule <jp@svrnty.io>
337 lines
11 KiB
Dart
337 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:animate_do/animate_do.dart';
|
|
import 'package:iconsax/iconsax.dart';
|
|
import 'package:getwidget/getwidget.dart';
|
|
|
|
class NavigationSidebar extends StatefulWidget {
|
|
final bool isExpanded;
|
|
final Function(String) onNavigate;
|
|
final String currentPage;
|
|
|
|
const NavigationSidebar({
|
|
super.key,
|
|
required this.isExpanded,
|
|
required this.onNavigate,
|
|
required this.currentPage,
|
|
});
|
|
|
|
@override
|
|
State<NavigationSidebar> createState() => _NavigationSidebarState();
|
|
}
|
|
|
|
class _NavigationSidebarState extends State<NavigationSidebar> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final width = widget.isExpanded ? 250.0 : 70.0;
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeInOut,
|
|
width: width,
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceContainerLow,
|
|
border: Border(
|
|
right: BorderSide(
|
|
color: colorScheme.outline.withValues(alpha: 0.2),
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Svrnty Logo Section
|
|
_buildLogoSection(colorScheme),
|
|
const SizedBox(height: 20),
|
|
|
|
// Navigation Menu Items
|
|
Expanded(
|
|
child: ListView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
children: [
|
|
_buildMenuItem(
|
|
icon: Iconsax.home,
|
|
title: 'Dashboard',
|
|
pageId: 'dashboard',
|
|
colorScheme: colorScheme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildMenuItem(
|
|
icon: Iconsax.hierarchy_square,
|
|
title: 'The Architech',
|
|
pageId: 'architech',
|
|
colorScheme: colorScheme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildMenuItem(
|
|
icon: Iconsax.cpu,
|
|
title: 'Agents',
|
|
pageId: 'agents',
|
|
colorScheme: colorScheme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildMenuItem(
|
|
icon: Iconsax.messages_3,
|
|
title: 'Conversations',
|
|
pageId: 'conversations',
|
|
colorScheme: colorScheme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildMenuItem(
|
|
icon: Iconsax.flash_1,
|
|
title: 'Executions',
|
|
pageId: 'executions',
|
|
colorScheme: colorScheme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildMenuItem(
|
|
icon: Iconsax.chart_square,
|
|
title: 'Analytics',
|
|
pageId: 'analytics',
|
|
colorScheme: colorScheme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildMenuItem(
|
|
icon: Iconsax.box,
|
|
title: 'Tools',
|
|
pageId: 'tools',
|
|
colorScheme: colorScheme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildMenuItem(
|
|
icon: Iconsax.setting_2,
|
|
title: 'Settings',
|
|
pageId: 'settings',
|
|
colorScheme: colorScheme,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// User Profile Section (bottom)
|
|
_buildUserSection(colorScheme),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLogoSection(ColorScheme colorScheme) {
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 20,
|
|
horizontal: widget.isExpanded ? 20 : 12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: colorScheme.outline.withValues(alpha: 0.2),
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: widget.isExpanded
|
|
? FadeIn(
|
|
duration: const Duration(milliseconds: 200),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primary,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Text(
|
|
'S',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 23,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Svrnty',
|
|
style: TextStyle(
|
|
fontSize: 20.7,
|
|
fontWeight: FontWeight.bold,
|
|
color: colorScheme.onSurface,
|
|
),
|
|
),
|
|
Text(
|
|
'Console',
|
|
style: TextStyle(
|
|
fontSize: 12.65,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primary,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Text(
|
|
'S',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 23,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMenuItem({
|
|
required IconData icon,
|
|
required String title,
|
|
required String pageId,
|
|
required ColorScheme colorScheme,
|
|
}) {
|
|
final isActive = widget.currentPage == pageId;
|
|
|
|
return FadeInLeft(
|
|
duration: const Duration(milliseconds: 300),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () => widget.onNavigate(pageId),
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: 14,
|
|
horizontal: widget.isExpanded ? 16 : 12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isActive
|
|
? colorScheme.primary.withValues(alpha: 0.25)
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: isActive
|
|
? colorScheme.primary.withValues(alpha: 0.7)
|
|
: Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
color: isActive
|
|
? Colors.white
|
|
: colorScheme.onSurfaceVariant,
|
|
size: 24,
|
|
),
|
|
if (widget.isExpanded) ...[
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: FadeIn(
|
|
duration: const Duration(milliseconds: 200),
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 17.25,
|
|
fontWeight:
|
|
isActive ? FontWeight.w600 : FontWeight.w400,
|
|
color: isActive
|
|
? Colors.white
|
|
: colorScheme.onSurface,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildUserSection(ColorScheme colorScheme) {
|
|
return Container(
|
|
padding: EdgeInsets.all(widget.isExpanded ? 16 : 12),
|
|
decoration: BoxDecoration(
|
|
border: Border(
|
|
top: BorderSide(
|
|
color: colorScheme.outline.withValues(alpha: 0.2),
|
|
width: 1,
|
|
),
|
|
),
|
|
),
|
|
child: widget.isExpanded
|
|
? FadeIn(
|
|
duration: const Duration(milliseconds: 200),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
GFAvatar(
|
|
radius: 18,
|
|
backgroundColor: colorScheme.primary.withValues(alpha: 0.3),
|
|
shape: GFAvatarShape.circle,
|
|
child: Icon(
|
|
Iconsax.user,
|
|
color: colorScheme.primary,
|
|
size: 18,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'Admin',
|
|
style: TextStyle(
|
|
fontSize: 14.95,
|
|
fontWeight: FontWeight.w600,
|
|
color: colorScheme.onSurface,
|
|
),
|
|
),
|
|
Text(
|
|
'admin@svrnty.ai',
|
|
style: TextStyle(
|
|
fontSize: 11.5,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: Center(
|
|
child: GFAvatar(
|
|
radius: 20,
|
|
backgroundColor: colorScheme.primary.withValues(alpha: 0.2),
|
|
shape: GFAvatarShape.circle,
|
|
child: Icon(
|
|
Iconsax.user,
|
|
color: colorScheme.primary,
|
|
size: 20,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|