import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:trainhub_flutter/core/constants/ui_constants.dart'; import 'package:trainhub_flutter/core/theme/app_colors.dart'; import 'package:trainhub_flutter/core/router/app_router.dart'; import 'package:trainhub_flutter/presentation/settings/ai_model_settings_controller.dart'; import 'package:trainhub_flutter/presentation/settings/ai_model_settings_state.dart'; @RoutePage() class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final modelState = ref.watch(aiModelSettingsControllerProvider); final controller = ref.read(aiModelSettingsControllerProvider.notifier); return Scaffold( backgroundColor: AppColors.surface, body: Column( children: [ // ── Top bar ────────────────────────────────────────────────────── _TopBar(onBack: () => context.router.maybePop()), // ── Scrollable content ────────────────────────────────────────── Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(UIConstants.pagePadding), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 680), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Page title Text( 'Settings', style: GoogleFonts.inter( fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary, letterSpacing: -0.3, ), ), const SizedBox(height: UIConstants.spacing32), // AI Models section _AiModelsSection( modelState: modelState, onDownload: controller.downloadAll, onValidate: controller.validateModels, ), const SizedBox(height: UIConstants.spacing32), // Knowledge Base section _KnowledgeBaseSection( onTap: () => context.router .push(const KnowledgeBaseRoute()), ), ], ), ), ), ), ), ], ), ); } } // ============================================================================= // Top bar // ============================================================================= class _TopBar extends StatelessWidget { const _TopBar({required this.onBack}); final VoidCallback onBack; @override Widget build(BuildContext context) { return Container( height: 52, padding: const EdgeInsets.symmetric(horizontal: UIConstants.spacing16), decoration: const BoxDecoration( color: AppColors.surfaceContainer, border: Border(bottom: BorderSide(color: AppColors.border)), ), child: Row( children: [ _IconBtn( icon: Icons.arrow_back_rounded, tooltip: 'Go back', onTap: onBack, ), const SizedBox(width: UIConstants.spacing12), Text( 'Settings', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ], ), ); } } // ============================================================================= // AI Models section // ============================================================================= class _AiModelsSection extends StatelessWidget { const _AiModelsSection({ required this.modelState, required this.onDownload, required this.onValidate, }); final AiModelSettingsState modelState; final VoidCallback onDownload; final VoidCallback onValidate; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section heading Text( 'AI Models', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textMuted, letterSpacing: 0.8, ), ), const SizedBox(height: UIConstants.spacing12), // Card Container( decoration: BoxDecoration( color: AppColors.surfaceContainer, borderRadius: BorderRadius.circular(UIConstants.borderRadius), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Status rows const _ModelRow( name: 'llama-server binary', description: 'llama.cpp inference server (build b8130)', icon: Icons.terminal_rounded, ), const Divider(height: 1, color: AppColors.border), const _ModelRow( name: 'Nomic Embed v1.5 Q4_K_M', description: 'Text embedding model (~300 MB)', icon: Icons.hub_outlined, ), const Divider(height: 1, color: AppColors.border), const _ModelRow( name: 'Qwen 2.5 7B Instruct Q4_K_M', description: 'Chat / reasoning model (~4.7 GB)', icon: Icons.psychology_outlined, ), // Divider before status / actions const Divider(height: 1, color: AppColors.border), Padding( padding: const EdgeInsets.all(UIConstants.spacing16), child: _StatusAndActions( modelState: modelState, onDownload: onDownload, onValidate: onValidate, ), ), ], ), ), ], ); } } // ============================================================================= // Single model info row // ============================================================================= class _ModelRow extends StatelessWidget { const _ModelRow({ required this.name, required this.description, required this.icon, }); final String name; final String description; final IconData icon; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, vertical: UIConstants.spacing12, ), child: Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: AppColors.surfaceContainerHigh, borderRadius: BorderRadius.circular(8), ), child: Icon(icon, size: 16, color: AppColors.textSecondary), ), const SizedBox(width: UIConstants.spacing12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.textPrimary, ), ), const SizedBox(height: 2), Text( description, style: GoogleFonts.inter( fontSize: 12, color: AppColors.textMuted, ), ), ], ), ), ], ), ); } } // ============================================================================= // Status badge + action buttons // ============================================================================= class _StatusAndActions extends StatelessWidget { const _StatusAndActions({ required this.modelState, required this.onDownload, required this.onValidate, }); final AiModelSettingsState modelState; final VoidCallback onDownload; final VoidCallback onValidate; @override Widget build(BuildContext context) { // While downloading, show progress UI if (modelState.isDownloading) { return _DownloadingView(modelState: modelState); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Status badge _StatusBadge(validated: modelState.areModelsValidated), if (modelState.errorMessage != null) ...[ const SizedBox(height: UIConstants.spacing12), _ErrorRow(message: modelState.errorMessage!), ], const SizedBox(height: UIConstants.spacing16), // Action buttons if (!modelState.areModelsValidated) _ActionButton( label: 'Download AI Models (~5 GB)', icon: Icons.download_rounded, color: AppColors.accent, textColor: AppColors.zinc950, onPressed: onDownload, ) else _ActionButton( label: 'Re-validate Files', icon: Icons.verified_outlined, color: Colors.transparent, textColor: AppColors.textSecondary, borderColor: AppColors.border, onPressed: onValidate, ), ], ); } } class _StatusBadge extends StatelessWidget { const _StatusBadge({required this.validated}); final bool validated; @override Widget build(BuildContext context) { final color = validated ? AppColors.success : AppColors.textMuted; final bgColor = validated ? AppColors.successMuted : AppColors.surfaceContainerHigh; final label = validated ? 'Ready' : 'Missing'; final icon = validated ? Icons.check_circle_outline : Icons.radio_button_unchecked; return Row( mainAxisSize: MainAxisSize.min, children: [ Text( 'Status: ', style: GoogleFonts.inter( fontSize: 13, color: AppColors.textSecondary, ), ), const SizedBox(width: UIConstants.spacing4), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: color), const SizedBox(width: 5), Text( label, style: GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.w600, color: color, ), ), ], ), ), ], ); } } class _DownloadingView extends StatelessWidget { const _DownloadingView({required this.modelState}); final AiModelSettingsState modelState; @override Widget build(BuildContext context) { final pct = (modelState.progress * 100).toStringAsFixed(1); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( modelState.currentTask, style: GoogleFonts.inter( fontSize: 13, color: AppColors.textSecondary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Text( '$pct %', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.accent, ), ), ], ), const SizedBox(height: UIConstants.spacing8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: modelState.progress, minHeight: 6, backgroundColor: AppColors.zinc800, valueColor: const AlwaysStoppedAnimation(AppColors.accent), ), ), if (modelState.errorMessage != null) ...[ const SizedBox(height: UIConstants.spacing12), _ErrorRow(message: modelState.errorMessage!), ], ], ); } } class _ErrorRow extends StatelessWidget { const _ErrorRow({required this.message}); final String message; @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( Icons.error_outline_rounded, color: AppColors.destructive, size: 14, ), const SizedBox(width: UIConstants.spacing8), Expanded( child: Text( message, style: GoogleFonts.inter( fontSize: 12, color: AppColors.destructive, height: 1.4, ), ), ), ], ); } } class _ActionButton extends StatefulWidget { const _ActionButton({ required this.label, required this.icon, required this.color, required this.textColor, required this.onPressed, this.borderColor, }); final String label; final IconData icon; final Color color; final Color textColor; final Color? borderColor; final VoidCallback onPressed; @override State<_ActionButton> createState() => _ActionButtonState(); } class _ActionButtonState extends State<_ActionButton> { bool _hovered = false; @override Widget build(BuildContext context) { final hasBorder = widget.borderColor != null; return MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: AnimatedContainer( duration: UIConstants.animationDuration, height: 40, decoration: BoxDecoration( color: hasBorder ? (_hovered ? AppColors.zinc800 : Colors.transparent) : (_hovered ? widget.color.withValues(alpha: 0.85) : widget.color), borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius), border: hasBorder ? Border.all(color: widget.borderColor!) : null, ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius), onTap: widget.onPressed, child: Padding( padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(widget.icon, size: 16, color: widget.textColor), const SizedBox(width: UIConstants.spacing8), Text( widget.label, style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, color: widget.textColor, ), ), ], ), ), ), ), ), ); } } // ============================================================================= // Generic icon button // ============================================================================= class _IconBtn extends StatefulWidget { const _IconBtn({ required this.icon, required this.onTap, this.tooltip = '', }); final IconData icon; final VoidCallback onTap; final String tooltip; @override State<_IconBtn> createState() => _IconBtnState(); } class _IconBtnState extends State<_IconBtn> { bool _hovered = false; @override Widget build(BuildContext context) { return Tooltip( message: widget.tooltip, child: MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: GestureDetector( onTap: widget.onTap, child: AnimatedContainer( duration: UIConstants.animationDuration, width: 32, height: 32, decoration: BoxDecoration( color: _hovered ? AppColors.zinc800 : Colors.transparent, borderRadius: BorderRadius.circular(6), ), child: Icon( widget.icon, size: 18, color: _hovered ? AppColors.textPrimary : AppColors.textSecondary, ), ), ), ), ); } } // ============================================================================= // Knowledge Base navigation section // ============================================================================= class _KnowledgeBaseSection extends StatelessWidget { const _KnowledgeBaseSection({required this.onTap}); final VoidCallback onTap; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Knowledge Base', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textMuted, letterSpacing: 0.8, ), ), const SizedBox(height: UIConstants.spacing12), _KnowledgeBaseCard(onTap: onTap), ], ); } } class _KnowledgeBaseCard extends StatefulWidget { const _KnowledgeBaseCard({required this.onTap}); final VoidCallback onTap; @override State<_KnowledgeBaseCard> createState() => _KnowledgeBaseCardState(); } class _KnowledgeBaseCardState extends State<_KnowledgeBaseCard> { bool _hovered = false; @override Widget build(BuildContext context) { return MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: GestureDetector( onTap: widget.onTap, child: AnimatedContainer( duration: UIConstants.animationDuration, decoration: BoxDecoration( color: _hovered ? AppColors.surfaceContainerHigh : AppColors.surfaceContainer, borderRadius: BorderRadius.circular(UIConstants.borderRadius), border: Border.all( color: _hovered ? AppColors.accent.withValues(alpha: 0.3) : AppColors.border, ), ), padding: const EdgeInsets.symmetric( horizontal: UIConstants.spacing16, vertical: UIConstants.spacing16, ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.accentMuted, borderRadius: BorderRadius.circular(10), ), child: const Icon( Icons.hub_outlined, color: AppColors.accent, size: 20, ), ), const SizedBox(width: UIConstants.spacing16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Manage Knowledge Base', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.textPrimary, ), ), const SizedBox(height: 3), Text( 'Add trainer notes to give the AI context-aware answers.', style: GoogleFonts.inter( fontSize: 12, color: AppColors.textMuted, ), ), ], ), ), Icon( Icons.chevron_right_rounded, color: _hovered ? AppColors.accent : AppColors.textMuted, size: 20, ), ], ), ), ), ); } }