import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart'; import 'package:ainoval/models/ai_model_group.dart'; import 'package:ainoval/models/user_ai_model_config_model.dart'; import 'package:ainoval/screens/settings/widgets/custom_model_dialog.dart'; import 'package:ainoval/screens/settings/widgets/model_group_list.dart'; import 'package:ainoval/screens/settings/widgets/provider_list.dart'; import 'package:ainoval/screens/settings/widgets/searchable_model_dropdown.dart'; import 'package:ainoval/widgets/common/top_toast.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/foundation.dart'; // Removed import causing linter error // import 'package:ai_config_repository/ai_config_repository.dart'; // Placeholder for localization, replace with your actual import // import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AiConfigForm extends StatefulWidget { // Callback when cancel is pressed // Optional: Callback on successful save if specific action needed besides hiding form // final VoidCallback? onSaveSuccess; const AiConfigForm({ super.key, required this.userId, required this.onCancel, this.configToEdit, // this.onSaveSuccess, }); final UserAIModelConfigModel? configToEdit; final String userId; final VoidCallback onCancel; @override State createState() => _AiConfigFormState(); } class _AiConfigFormState extends State { final _formKey = GlobalKey(); late TextEditingController _aliasController; late TextEditingController _apiKeyController; late TextEditingController _apiEndpointController; String? _selectedProvider; String? _selectedModel; ModelListingCapability? _providerCapability; // New: Store capability bool _isLoadingProviders = false; bool _isLoadingModels = false; bool _isTestingApiKey = false; // New: Track API key testing bool _apiKeyTestSuccess = false; // New: Track API key test success for current provider bool _isSaving = false; // Track internal saving state bool _showApiKey = false; // 控制API Key是否显示 List _providers = []; List _models = []; // 校验错误状态 bool _providerError = false; bool _modelError = false; bool _apiKeyError = false; String? _providerErrorText; String? _modelErrorText; String? _apiKeyErrorText; bool get _isEditMode => widget.configToEdit != null; @override void initState() { super.initState(); // Initialize controllers _aliasController = TextEditingController(text: widget.configToEdit?.alias ?? ''); // 编辑模式下回显API Key,新增模式下保持空白 _apiKeyController = TextEditingController( text: _isEditMode ? (widget.configToEdit?.apiKey ?? '') : '' ); _apiEndpointController = TextEditingController(text: widget.configToEdit?.apiEndpoint ?? ''); // Initialize state based on edit mode if (_isEditMode) { _selectedProvider = widget.configToEdit?.provider; _selectedModel = widget.configToEdit?.modelName; // Don't prefill API Key from edit mode _apiEndpointController.text = widget.configToEdit?.apiEndpoint ?? ''; _aliasController.text = widget.configToEdit?.alias ?? ''; } else { _selectedProvider = null; _selectedModel = null; _providers = []; _models = []; _apiEndpointController.text = ''; _apiKeyController.text = ''; _aliasController.text = ''; } // Use context safely after first frame WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; // Check mount status final bloc = context.read(); // Pre-populate provider list from bloc state if available if (_providers.isEmpty) { _providers = bloc.state.availableProviders; } // Always load providers on init _loadProviders(); // If a provider is selected (edit mode or restored state), load its capability // 在编辑模式下跳过能力加载和自动填充,避免不必要的API Key验证 if (_selectedProvider != null && !_isEditMode) { print("InitState: Provider '$_selectedProvider' selected, loading capability."); bloc.add(LoadProviderCapability(providerName: _selectedProvider!)); // Model loading will now be handled by the BlocListener based on capability // Also try to load default config info _autoFillApiInfo(_selectedProvider!); } // --- Trigger loading --- // Always try to load providers when the form inits, // as the list might be stale or empty (especially in add mode). // The BlocListener will handle the loading indicator state. //_loadProviders(); // Moved up // Model loading logic is now primarily driven by provider capability // and API key testing results handled in the BlocListener. }); } @override void dispose() { _aliasController.dispose(); _apiKeyController.dispose(); _apiEndpointController.dispose(); // Don't clear models here as the Bloc state might be needed elsewhere // context.read().add(ClearProviderModels()); super.dispose(); } void _loadProviders() { if (!mounted) return; // Check if widget is still in the tree setState(() { _isLoadingProviders = true; }); context.read().add(const LoadAvailableProviders()); // Corrected call } void _loadModels(String provider) { if (!mounted) return; print("UI triggered _loadModels for $provider"); // Debug log setState(() { _isLoadingModels = true; // Reset model only if provider actually changes (don't reset in edit mode init) if (_selectedProvider != provider) { _selectedModel = null; } _models = []; // Clear previous models for the dropdown }); context.read().add(LoadModelsForProvider(provider: provider)); } void _submitForm() { // 清除之前的错误状态 setState(() { _providerError = false; _modelError = false; _apiKeyError = false; _providerErrorText = null; _modelErrorText = null; _apiKeyErrorText = null; }); // 执行校验 bool hasError = false; // 在添加模式下校验提供商选择 if (!_isEditMode && _selectedProvider == null) { setState(() { _providerError = true; _providerErrorText = '请选择一个提供商'; }); hasError = true; } // 在添加模式下校验模型选择 if (!_isEditMode && _selectedModel == null) { setState(() { _modelError = true; _modelErrorText = '请选择一个模型'; }); hasError = true; } // 校验API Key(在添加模式下,如果提供商需要API Key) if (!_isEditMode && _providerCapability == ModelListingCapability.listingWithKey && _apiKeyController.text.trim().isEmpty) { setState(() { _apiKeyError = true; _apiKeyErrorText = '该提供商需要 API 密钥'; }); hasError = true; } // 检查重复的已验证配置(仅在添加模式) if (!_isEditMode && _selectedProvider != null && _selectedModel != null) { final existingConfigs = context.read().state.configs; final isDuplicateValidated = existingConfigs.any((config) => config.provider == _selectedProvider && config.modelName == _selectedModel && config.isValidated); if (isDuplicateValidated) { setState(() { _modelError = true; _modelErrorText = '已存在该模型的已验证配置,无法重复添加'; }); hasError = true; } } // 如果有错误,不执行提交 if (hasError) { return; } // 执行表单验证 if (_formKey.currentState!.validate()) { setState(() { _isSaving = true; }); final bloc = context.read(); // 处理API密钥 String? apiKey = _apiKeyController.text.trim(); if (apiKey.isEmpty) { apiKey = null; } if (_isEditMode) { bloc.add(UpdateAiConfig( userId: widget.userId, configId: widget.configToEdit!.id, alias: _aliasController.text.trim().isEmpty ? null : _aliasController.text.trim(), apiKey: apiKey, // Pass null if empty to potentially clear/not update apiEndpoint: _apiEndpointController.text.trim(), // Send empty string to clear )); } else { bloc.add(AddAiConfig( userId: widget.userId, provider: _selectedProvider!, modelName: _selectedModel!, apiKey: apiKey ?? "", // Backend likely expects non-null, pass empty string alias: _aliasController.text.trim().isEmpty ? _selectedModel // Default alias to model name if empty : _aliasController.text.trim(), apiEndpoint: _apiEndpointController.text.trim(), )); } // The BlocListener in SettingsPanel will handle hiding the form on success/error } } // 处理模型选择 void _handleModelSelected(String model) { setState(() { _selectedModel = model; // 清除模型选择错误状态 _modelError = false; _modelErrorText = null; // 设置别名默认为模型名称 if (_aliasController.text.isEmpty) { _aliasController.text = model; } }); } // 处理自定义模型添加 void _handleAddCustomModel() { if (_selectedProvider == null) return; showDialog( context: context, builder: (context) => CustomModelDialog( providerName: _selectedProvider!, onConfirm: (modelName, modelAlias, apiEndpoint) async { // 检查 API 密钥 final apiKey = _apiKeyController.text.trim(); if (apiKey.isEmpty) { TopToast.warning(context, '请先输入 API 密钥再添加自定义模型'); return; } // 立即保存配置到后端 try { AppLogger.i('AiConfigForm', '开始添加自定义模型: $_selectedProvider/$modelName'); // 显示加载状态 setState(() { _isSaving = true; }); // 触发添加自定义模型并验证事件 context.read().add(AddCustomModelAndValidate( userId: widget.userId, provider: _selectedProvider!, modelName: modelName, apiKey: apiKey, alias: modelAlias, apiEndpoint: apiEndpoint?.isEmpty == true ? null : apiEndpoint, )); // 显示成功提示 TopToast.info(context, '自定义模型 $modelName 已添加,正在验证连接...'); } catch (e) { AppLogger.e('AiConfigForm', '添加自定义模型失败', e); setState(() { _isSaving = false; }); TopToast.error(context, '添加自定义模型失败: ${e.toString()}'); } }, ), ); } // 检查是否存在已验证的相同模型 bool _isDuplicateValidatedModel() { if (_isEditMode || _selectedProvider == null || _selectedModel == null) { return false; } final existingConfigs = context.read().state.configs; return existingConfigs.any((config) => config.provider == _selectedProvider && config.modelName == _selectedModel && config.isValidated); } // 获取已验证模型列表 List _getVerifiedModels(String provider) { final existingConfigs = context.read().state.configs; return existingConfigs .where((config) => config.provider == provider && config.isValidated) .map((config) => config.modelName) .toList(); } // Modify provider selection handler void _handleProviderSelected(String provider) { print('️Provider selected: $provider'); if (provider != _selectedProvider) { setState(() { _selectedProvider = provider; _selectedModel = null; // Reset model selection _providerCapability = null; // Reset capability _apiKeyTestSuccess = false; // Reset API key test status _isTestingApiKey = false; // Reset testing flag _models = []; // Clear model list _isLoadingModels = false; // Reset loading models flag // 清除错误状态 _providerError = false; _modelError = false; _apiKeyError = false; _providerErrorText = null; _modelErrorText = null; _apiKeyErrorText = null; // Clear previous provider's info only in Add mode if (!_isEditMode) { _apiEndpointController.text = ''; _apiKeyController.text = ''; _aliasController.text = ''; // Clear alias too } _showApiKey = false; // Hide API key on provider change }); // Trigger loading capability for the new provider context.read().add(LoadProviderCapability(providerName: provider)); // Trigger auto-fill for the new provider // 编辑模式下不触发自动填充 if (!_isEditMode) { _autoFillApiInfo(provider); } } } // 切换API Key的显示/隐藏 void _toggleApiKeyVisibility() { setState(() { _showApiKey = !_showApiKey; }); } // 自动填充API信息 void _autoFillApiInfo(String provider) { // 编辑模式下不需要自动填充,避免触发不必要的API Key验证 if (_isEditMode) return; // 发送获取该提供商默认配置的事件 // 实际的填充操作会在BlocListener中根据状态变化处理 print('⚠️ 调用_autoFillApiInfo,provider=$provider'); context.read().add(GetProviderDefaultConfig(provider: provider)); } // New method to handle API Key test button press void _testApiKey() { final apiKey = _apiKeyController.text.trim(); final apiEndpoint = _apiEndpointController.text.trim().isEmpty ? null : _apiEndpointController.text.trim(); if (_selectedProvider != null && apiKey.isNotEmpty) { // Set testing state in UI immediately // No need to call setState here as BlocListener will handle it context.read().add(TestApiKey( providerName: _selectedProvider!, apiKey: apiKey, apiEndpoint: apiEndpoint, )); } else { // Show feedback if provider or key is missing TopToast.warning(context, '请先选择提供商并输入 API 密钥'); } } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (!mounted) return; bool needsSetState = false; // --- Provider Loading & List Update --- if (_isLoadingProviders && (state.availableProviders.isNotEmpty || state.errorMessage != null && state.status != AiConfigStatus.loading)) { _isLoadingProviders = false; needsSetState = true; } if (!listEquals(_providers, state.availableProviders)) { _providers = state.availableProviders; needsSetState = true; } // --- Provider Capability Update --- if (state.providerCapability != _providerCapability && state.selectedProviderForModels == _selectedProvider) { _providerCapability = state.providerCapability; print("Listener: Capability updated for $_selectedProvider: $_providerCapability"); needsSetState = true; // Note: Model loading based on capability is handled in the BLoC event handler itself now. } // --- Model Loading & List Update --- if (state.selectedProviderForModels == _selectedProvider) { if (_isLoadingModels && (state.modelsForProvider.isNotEmpty || state.errorMessage != null)) { _isLoadingModels = false; needsSetState = true; } if (!listEquals(_models, state.modelsForProvider)) { _models = state.modelsForProvider; // If models updated (e.g., after API test), re-validate selected model if (!_models.contains(_selectedModel)) { _selectedModel = null; } needsSetState = true; } } else if (_isLoadingModels) { // If the selected provider changed while models were loading, stop loading indicator _isLoadingModels = false; needsSetState = true; } // --- API 密钥测试状态更新 --- if (state.isTestingApiKey != _isTestingApiKey) { _isTestingApiKey = state.isTestingApiKey; // If we *start* testing (used for loading models with key), set loading state if (_isTestingApiKey) { _isLoadingModels = true; } needsSetState = true; } // Check if success is for the *currently selected* provider and non-null final testSuccessForCurrentProvider = state.apiKeyTestSuccessProvider != null && state.apiKeyTestSuccessProvider == _selectedProvider; if (testSuccessForCurrentProvider != _apiKeyTestSuccess) { _apiKeyTestSuccess = testSuccessForCurrentProvider; if (_apiKeyTestSuccess) { _isLoadingModels = false; // <-- Reset loading state on success } needsSetState = true; } // 处理 API 密钥测试错误 if (state.apiKeyTestError != null) { print("Listener: API Key test FAILED for $_selectedProvider: ${state.apiKeyTestError}"); _isLoadingModels = false; // <-- Reset loading state on error TopToast.error(context, 'API 密钥测试失败: ${state.apiKeyTestError}'); // Clear the error in the bloc state? This should maybe be done in the bloc itself after emitting. // context.read().add(ClearApiKeyTestError()); // Need this event/logic in BLoC needsSetState = true; // Need to rebuild to potentially remove loading indicator } // --- Default Config Auto-fill Update --- if (_selectedProvider != null) { final defaultConfig = state.providerDefaultConfigs[_selectedProvider!]; if (defaultConfig != null && defaultConfig.id.isNotEmpty) { if (!_isEditMode) { bool filledSomething = false; if (_apiEndpointController.text.isEmpty && defaultConfig.apiEndpoint.isNotEmpty) { _apiEndpointController.text = defaultConfig.apiEndpoint; filledSomething = true; } if (_apiKeyController.text.isEmpty && (defaultConfig.apiKey?.isNotEmpty ?? false)) { _apiKeyController.text = defaultConfig.apiKey!; _showApiKey = true; filledSomething = true; // If auto-filled API key, consider it "tested" for UI purposes, // but a real test might still be needed depending on workflow. // _apiKeyTestSuccess = true; // Maybe set this? Or require manual test? Let's require manual test for now. } if(filledSomething) needsSetState = true; } } } // --- Saving State Update --- if (_isSaving && state.actionStatus != AiConfigActionStatus.loading) { if (state.actionStatus == AiConfigActionStatus.success) { widget.onCancel(); } // Error toast is handled by the listener in SettingsPanel _isSaving = false; needsSetState = true; } if (needsSetState) { setState(() {}); } }, child: Scaffold( backgroundColor: Colors.transparent, body: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 标题 Text( _isEditMode ? '编辑模型服务' : '添加新模型服务', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 16), // 主要内容区域 - 左右布局 Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 左侧提供商列表 if (!_isEditMode) // 在编辑模式下不显示左侧列表 SizedBox( width: 180, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: ProviderList( providers: _providers, selectedProvider: _selectedProvider, onProviderSelected: _handleProviderSelected, ), ), // 提供商选择错误提示 if (_providerError && _providerErrorText != null) Padding( padding: const EdgeInsets.only(top: 4, left: 8), child: Text( _providerErrorText!, style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 12, ), ), ), ], ), ), if (!_isEditMode) const SizedBox(width: 16), // 右侧配置区域 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Provider显示(仅编辑模式) if (_isEditMode && _selectedProvider != null) Padding( padding: const EdgeInsets.only(bottom: 16), child: Text( '提供商: $_selectedProvider', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), // 模型搜索框(仅添加模式) if (!_isEditMode && _selectedProvider != null) Padding( padding: const EdgeInsets.only(bottom: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '选择模型', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: _modelError ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.onSurface.withOpacity(0.8), ), ), const SizedBox(height: 4), Container( height: 36, decoration: _modelError ? BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.error, width: 1.5, ), ) : null, child: SearchableModelDropdown( models: _models, onModelSelected: _handleModelSelected, hintText: '搜索可用模型', ), ), // 模型选择错误提示 if (_modelError && _modelErrorText != null) Padding( padding: const EdgeInsets.only(top: 4), child: Text( _modelErrorText!, style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 12, ), ), ), ], ), ), // 已选模型显示(仅添加模式) if (!_isEditMode && _selectedModel != null) Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( '已选模型: $_selectedModel', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold, ), ), ), // 模型显示(仅编辑模式) if (_isEditMode && _selectedModel != null) Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( '模型: $_selectedModel', style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), // 别名输入框 Padding( padding: const EdgeInsets.only(bottom: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '别名 (可选)', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), ), ), const SizedBox(height: 6), SizedBox( height: 36, child: TextFormField( controller: _aliasController, style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( hintText: '例如:我的 ${_selectedModel ?? '模型'}', hintStyle: TextStyle( fontSize: 13, color: Theme.of(context).hintColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: Theme.of(context).colorScheme.outline.withOpacity(0.3), width: 1.0, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: Theme.of(context).colorScheme.outline.withOpacity(0.3), width: 1.0, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: Theme.of(context).colorScheme.primary, width: 1.5, ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 0 ), filled: true, fillColor: Theme.of(context).brightness == Brightness.dark ? Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3) : Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.7), ), ), ), ], ), ), // API Key输入框 Padding( padding: const EdgeInsets.only(bottom: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'API 密钥', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: _apiKeyError ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.onSurface.withOpacity(0.8), ), ), const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.center, // Align items vertically children: [ Expanded( child: SizedBox( height: 36, // Keep height consistent child: TextFormField( controller: _apiKeyController, obscureText: !_showApiKey, style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( hintText: _isEditMode ? '当前API密钥 (可修改)' : '输入您的API密钥', hintStyle: TextStyle( fontSize: 13, color: Theme.of(context).hintColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: _apiKeyError ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.outline.withOpacity(0.3), width: _apiKeyError ? 1.5 : 1.0, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: _apiKeyError ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.outline.withOpacity(0.3), width: _apiKeyError ? 1.5 : 1.0, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: _apiKeyError ? Theme.of(context).colorScheme.error : Theme.of(context).colorScheme.primary, width: 1.5, ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 0, // Adjust vertical padding if needed ), filled: true, fillColor: Theme.of(context).brightness == Brightness.dark ? Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3) : Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.7), suffixIcon: IconButton( icon: Icon( _showApiKey ? Icons.visibility_off : Icons.visibility, size: 18, ), onPressed: _toggleApiKeyVisibility, ), ), validator: (value) { // Require API key in add mode only if provider capability mandates it if (!_isEditMode && _providerCapability == ModelListingCapability.listingWithKey && (value == null || value.trim().isEmpty)) { return '需要 API 密钥'; } return null; }, onChanged: (_) { // Reset test success status if key changes if (_apiKeyTestSuccess) { setState(() { _apiKeyTestSuccess = false; }); } // 清除API Key错误状态 if (_apiKeyError) { setState(() { _apiKeyError = false; _apiKeyErrorText = null; }); } // Trigger rebuild to potentially enable/disable test button setState(() {}); }, ), ), ), // API 密钥测试按钮与状态 if (_selectedProvider != null && _providerCapability == ModelListingCapability.listingWithKey) Padding( padding: const EdgeInsets.only(left: 8.0), child: SizedBox( height: 36, // Match TextFormField height child: _isTestingApiKey ? const SizedBox( // Show loading indicator, but don't rebuild on test success, to avoid flicker width: 36, height: 36, child: Center(child: SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))) ) : (_apiKeyTestSuccess ? const Tooltip( message: 'API 密钥已验证', child: Icon( // Show success icon Icons.check_circle, color: Colors.green, size: 24), ) : TextButton( // Show test button // Disable button if API key field is empty onPressed: _apiKeyController.text.trim().isEmpty ? null : _testApiKey, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12), minimumSize: Size(0, 36), // Ensure height matches // Dim text color if disabled foregroundColor: _apiKeyController.text.trim().isEmpty ? Theme.of(context).disabledColor : null, ), child: const Text('测试', style: TextStyle(fontSize: 13)), ) ), ), ), ], ), // API 密钥错误提示 if (_apiKeyError && _apiKeyErrorText != null) Padding( padding: const EdgeInsets.only(top: 4), child: Text( _apiKeyErrorText!, style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 12, ), ), ), // Auto-fill prompt adjusted if (!_isEditMode && _selectedProvider != null && _providerCapability == ModelListingCapability.listingWithKey && !_apiKeyError) BlocBuilder( builder: (context, state) { final defaultConfig = state.getProviderDefaultConfig(_selectedProvider!); final hasFilledApiKey = _apiKeyController.text.isNotEmpty; // Show prompt only if key was auto-filled AND not yet tested successfully if (defaultConfig != null && defaultConfig.id.isNotEmpty && (defaultConfig.apiKey?.isNotEmpty ?? false) && hasFilledApiKey && !_apiKeyTestSuccess) { return Padding( padding: const EdgeInsets.only(top: 4), child: Row( children: [ Icon(Icons.info_outline, size: 14, color: Colors.blue), const SizedBox(width: 4), Expanded( child: Text('已自动填充API密钥,请测试连接', style: TextStyle(fontSize: 12, color: Colors.blue)), ), ], ), ); } return const SizedBox.shrink(); }, ), ], ), ), // API Endpoint输入框 Padding( padding: const EdgeInsets.only(bottom: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'API 接口地址(可选)', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), ), ), const SizedBox(height: 4), Row( children: [ Expanded( child: SizedBox( height: 36, child: TextFormField( controller: _apiEndpointController, style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( hintText: '例如:https://api.openai.com/v1', hintStyle: TextStyle( fontSize: 13, color: Theme.of(context).hintColor.withOpacity(0.7), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: Theme.of(context).colorScheme.outline.withOpacity(0.3), width: 1.0, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: Theme.of(context).colorScheme.outline.withOpacity(0.3), width: 1.0, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: Theme.of(context).colorScheme.primary, width: 1.5, ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 0 ), filled: true, fillColor: Theme.of(context).brightness == Brightness.dark ? Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.3) : Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.7), ), ), ), ), ], ), // 如果有该提供商的已配置API地址,显示提示 if (!_isEditMode && _selectedProvider != null) BlocBuilder( builder: (context, state) { final defaultConfig = state.getProviderDefaultConfig(_selectedProvider!); if (defaultConfig != null && defaultConfig.id.isNotEmpty && defaultConfig.apiEndpoint.isNotEmpty) { return Padding( padding: const EdgeInsets.only(top: 4), child: Row( children: [ Icon( Icons.info_outline, size: 14, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 4), Expanded( child: Text( '已使用该提供商的API地址: ${defaultConfig.apiEndpoint}', style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.primary, ), ), ), ], ), ); } return const SizedBox.shrink(); }, ), ], ), ), // 模型分组列表(仅在选择了提供商时显示) if (!_isEditMode && _selectedProvider != null && !_isLoadingModels) Expanded( // 将原来的Column改为Expanded child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 10.0, bottom: 4.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('可用模型', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), fontSize: 13, )), Row( mainAxisSize: MainAxisSize.min, children: [ // 刷新按钮 IconButton( icon: const Icon(Icons.refresh, size: 16), onPressed: () { if (_selectedProvider != null) { // 清除该提供商的缓存并重新加载 context.read().add(ClearModelsCache(provider: _selectedProvider)); context.read().add(LoadModelsForProvider(provider: _selectedProvider!)); } }, tooltip: '刷新模型列表', style: IconButton.styleFrom( padding: const EdgeInsets.all(4), minimumSize: const Size(24, 24), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), const SizedBox(width: 4), // 添加自定义模型按钮 TextButton.icon( icon: const Icon(Icons.add, size: 14), label: const Text('添加自定义模型', style: TextStyle(fontSize: 12)), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), minimumSize: const Size(0, 30), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: _handleAddCustomModel, ), ], ), ], ), ), // Container to provide background and border for the list area Expanded( // 将Container包装在Expanded中,使其填充剩余空间 child: Container( width: double.infinity, decoration: BoxDecoration( color: Theme.of(context).brightness == Brightness.dark ? Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.7), borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.2), width: 0.5, ), ), child: BlocBuilder( builder: (context, state) { // Safely access model group using the selected provider key // NOTE: Assuming AIModelGroup and ModelListingCapability are correctly defined/imported elsewhere // after resolving the 'ai_config_repository' dependency. final modelGroup = _selectedProvider != null ? state.modelGroups[_selectedProvider!] : null; final currentCapability = _providerCapability; // Use local capability state // 获取该提供商下已经验证的模型列表 final verifiedModels = _selectedProvider != null ? _getVerifiedModels(_selectedProvider!) : []; if (_isLoadingModels) { return const Center( child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator()), ); } // Check if model groups are available if (modelGroup != null && modelGroup.groups.isNotEmpty) { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: ModelGroupList( modelGroup: modelGroup, onModelSelected: _handleModelSelected, selectedModel: _selectedModel, verifiedModels: verifiedModels, // 传递已验证模型列表 ), ), ); } else { // Show message if no models are available or conditions not met String message = '该提供商没有可用的模型。'; // Check if capability requires API key and if it has been tested successfully // if (currentCapability == ModelListingCapability.listingWithKey && !_apiKeyTestSuccess) { // message = '请先成功测试 API Key 以加载模型列表。'; // } else if (currentCapability == ModelListingCapability.noListing) { // message = '该提供商不支持自动获取模型列表。'; // } // ^^^ Commented out capability check as ModelListingCapability might be undefined now // Fallback message if capability check is removed/unavailable if (modelGroup == null || modelGroup.groups.isEmpty) { if (_selectedProvider != null) { // More specific message if provider selected but no models message = '未能加载模型列表。如果需要 API Key,请确保已成功测试。'; } else { message = '请先选择一个提供商。'; } } return Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( message, textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: Theme.of(context).hintColor) ), const SizedBox(height: 16), FilledButton.icon( icon: const Icon(Icons.add, size: 16), label: const Text('添加自定义模型'), onPressed: _handleAddCustomModel, ), ], ), ), ); } }, ), ), ), ], ), ), // 加载指示器 if (!_isEditMode && _selectedProvider != null && _isLoadingModels) Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 10.0, bottom: 4.0), child: Text('可用模型', style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), fontSize: 13, )), ), Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text( '正在加载 $_selectedProvider 的模型列表...', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), ), ), ], ), ), ), ], ), ), ], // End of right column children ), ), // End of Expanded (Right Side) ], // End of Row children ), // End of Row ), // End of Expanded (Main Content Area) // 底部按钮区域 Padding( padding: const EdgeInsets.only(top: 16), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ // 调试按钮 if (!kReleaseMode) // 只在调试模式显示 TextButton( onPressed: () { final configs = context.read().state.providerDefaultConfigs; print('⚠️ 当前所有提供商默认配置:'); configs.forEach((provider, config) { print('⚠️ 提供商=$provider, configId=${config.id}, hasApiKey=${config.apiKey != null}'); }); }, child: const Text('打印配置', style: TextStyle(fontSize: 12, color: Colors.grey)), ), const Spacer(), OutlinedButton( onPressed: _isSaving ? null : widget.onCancel, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), side: BorderSide( color: Theme.of(context).colorScheme.outline.withOpacity(0.3), width: 1.0, ), ), child: const Text('取消', style: TextStyle(fontSize: 13)), ), const SizedBox(width: 12), ElevatedButton( onPressed: _isSaving ? null : _submitForm, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Theme.of(context).colorScheme.onPrimary, elevation: 1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: _isSaving ? const SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : Text(_isEditMode ? '保存更改' : '添加', style: const TextStyle(fontSize: 13)), ), ], ), ), ], // End of Form children ), // End of Form ), // End of Container ), // End of Body ), // End of Scaffold ); } } // Helper function to get model initial (potentially update logic if needed) String _getModelInitial(String modelName) { if (modelName.isEmpty) return '?'; // Simple initial, might need refinement for complex names return modelName[0].toUpperCase(); } // Helper function to get model color (can stay based on id or name) Color _getModelColor(String modelId) { // Use a hash of the model ID to generate a consistent color final int hash = modelId.hashCode; // Use HSLColor for better control over saturation and lightness return HSLColor.fromAHSL( 1.0, // Alpha (hash % 360).toDouble(), // Hue (0-360) 0.6, // Saturation (adjust as needed, 0.6 is moderately saturated) 0.5, // Lightness (adjust as needed, 0.5 is mid-lightness) ).toColor(); }