import 'package:flutter/material.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/presentation/settings/ai_model_settings_state.dart'; import 'package:trainhub_flutter/presentation/settings/widgets/settings_action_button.dart'; class AiModelsSection extends StatelessWidget { const AiModelsSection({ super.key, 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: [ Text( 'AI Models', style: GoogleFonts.inter( fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.textMuted, letterSpacing: 0.8, ), ), const SizedBox(height: UIConstants.spacing12), Container( decoration: BoxDecoration( color: AppColors.surfaceContainer, borderRadius: BorderRadius.circular(UIConstants.borderRadius), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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, ), const Divider(height: 1, color: AppColors.border), Padding( padding: const EdgeInsets.all(UIConstants.spacing16), child: _StatusAndActions( modelState: modelState, onDownload: onDownload, onValidate: onValidate, ), ), ], ), ), ], ); } } 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, ), ), ], ), ), ], ), ); } } 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) { if (modelState.isDownloading) { return _DownloadingView(modelState: modelState); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _StatusBadge(validated: modelState.areModelsValidated), if (modelState.errorMessage != null) ...[ const SizedBox(height: UIConstants.spacing12), ErrorRow(message: modelState.errorMessage!), ], const SizedBox(height: UIConstants.spacing16), if (!modelState.areModelsValidated) SettingsActionButton( label: 'Download AI Models (~5 GB)', icon: Icons.download_rounded, color: AppColors.accent, textColor: AppColors.zinc950, onPressed: onDownload, ) else SettingsActionButton( 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({super.key, 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, ), ), ), ], ); } }