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/data/services/ai_settings_service.dart'; import 'package:trainhub_flutter/injection.dart'; /// Lets the user pick which AI backend answers chat: the bundled local model /// or a cloud API (Anthropic / OpenAI / Gemini) with their own key. class AiProviderSection extends StatefulWidget { const AiProviderSection({super.key}); @override State createState() => _AiProviderSectionState(); } class _AiProviderSectionState extends State { late final AiSettingsService _service = getIt(); late AiProvider _provider; late final TextEditingController _apiKeyController; late final TextEditingController _modelController; late final TextEditingController _baseUrlController; bool _obscureKey = true; bool _saved = false; @override void initState() { super.initState(); _provider = _service.settings.provider; _apiKeyController = TextEditingController( text: _service.settings.apiKeys[_provider.name] ?? '', ); _modelController = TextEditingController( text: _service.settings.modelFor(_provider), ); _baseUrlController = TextEditingController( text: _service.settings.baseUrlFor(_provider), ); } @override void dispose() { _apiKeyController.dispose(); _modelController.dispose(); _baseUrlController.dispose(); super.dispose(); } void _onProviderChanged(AiProvider? provider) { if (provider == null) return; setState(() { _provider = provider; _apiKeyController.text = _service.settings.apiKeys[provider.name] ?? ''; _modelController.text = _service.settings.modelFor(provider); _baseUrlController.text = _service.settings.baseUrlFor(provider); _saved = false; }); } Future _save() async { final settings = _service.settings; await _service.save( settings.copyWith( provider: _provider, apiKeys: { ...settings.apiKeys, _provider.name: _apiKeyController.text.trim(), }, models: { ...settings.models, _provider.name: _modelController.text.trim(), }, baseUrls: { ...settings.baseUrls, _provider.name: _baseUrlController.text.trim(), }, ), ); if (mounted) setState(() => _saved = true); } @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: AppColors.surfaceContainer, borderRadius: UIConstants.cardBorderRadius, border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(UIConstants.spacing16), child: Row( children: [ const Icon( Icons.cloud_outlined, size: 18, color: AppColors.accent, ), const SizedBox(width: 10), Text( 'AI PROVIDER', style: GoogleFonts.chakraPetch( fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 1, color: AppColors.textPrimary, ), ), ], ), ), const Divider(height: 1, color: AppColors.border), Padding( padding: const EdgeInsets.all(UIConstants.spacing16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Chat backend. The knowledge base always uses the local ' 'embedding model — your notes never leave this machine. ' 'With a cloud provider, chat messages plus your exercise ' 'and plan names are sent to that provider.', style: GoogleFonts.inter( fontSize: 12, color: AppColors.textSecondary, height: 1.5, ), ), const SizedBox(height: UIConstants.spacing16), DropdownButtonFormField( initialValue: _provider, onChanged: _onProviderChanged, dropdownColor: AppColors.surfaceContainerHigh, style: GoogleFonts.inter( fontSize: 13, color: AppColors.textPrimary, ), decoration: const InputDecoration(labelText: 'Provider'), items: [ for (final p in AiProvider.values) DropdownMenuItem(value: p, child: Text(p.label)), ], ), if (_provider.isSelfHosted) ...[ const SizedBox(height: UIConstants.spacing12), TextField( controller: _baseUrlController, onChanged: (_) => setState(() => _saved = false), style: GoogleFonts.jetBrainsMono( fontSize: 12, color: AppColors.textPrimary, ), decoration: InputDecoration( labelText: 'Server URL', helperText: 'Default: ${_provider.defaultBaseUrl}', helperStyle: GoogleFonts.inter( fontSize: 11, color: AppColors.textMuted, ), ), ), ], if (_provider.needsApiKey) ...[ const SizedBox(height: UIConstants.spacing12), TextField( controller: _apiKeyController, obscureText: _obscureKey, onChanged: (_) => setState(() => _saved = false), style: GoogleFonts.jetBrainsMono( fontSize: 12, color: AppColors.textPrimary, ), decoration: InputDecoration( labelText: 'API key', suffixIcon: IconButton( icon: Icon( _obscureKey ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 16, ), onPressed: () => setState(() => _obscureKey = !_obscureKey), ), ), ), ], if (_provider != AiProvider.local) ...[ const SizedBox(height: UIConstants.spacing12), TextField( controller: _modelController, onChanged: (_) => setState(() => _saved = false), style: GoogleFonts.jetBrainsMono( fontSize: 12, color: AppColors.textPrimary, ), decoration: InputDecoration( labelText: 'Model', helperText: _provider == AiProvider.ollama ? 'Name of a model pulled in Ollama, ' 'e.g. qwen3:4b, llama3.1' : 'Default: ${_provider.defaultModel}', helperStyle: GoogleFonts.inter( fontSize: 11, color: AppColors.textMuted, ), ), ), ], const SizedBox(height: UIConstants.spacing16), Row( children: [ FilledButton(onPressed: _save, child: const Text('SAVE')), if (_saved) ...[ const SizedBox(width: UIConstants.spacing12), const Icon( Icons.check_circle_outline, size: 16, color: AppColors.success, ), const SizedBox(width: 6), Text( 'Saved', style: GoogleFonts.inter( fontSize: 12, color: AppColors.success, ), ), ], ], ), ], ), ), ], ), ); } }