import 'package:flutter/material.dart'; import '../../core/ai_service.dart'; import '../../core/app_state.dart'; import '../../core/app_theme.dart'; import '../../core/models.dart'; import '../../widgets/app_widgets.dart'; String aiProviderLabel(AiProviderType provider) => switch (provider) { AiProviderType.mock => '内置练习模式', AiProviderType.openAi => 'OpenAI API', AiProviderType.gemini => 'Gemini API', AiProviderType.compatible => 'OpenAI 兼容', }; /// AI 服务:服务类型、接口地址、模型与 API Key。 class AiServiceView extends StatefulWidget { const AiServiceView({super.key, required this.state}); final AppState state; @override State createState() => _AiServiceViewState(); } class _AiServiceViewState extends State { late final endpoint = TextEditingController(text: widget.state.aiEndpoint); late final model = TextEditingController(text: widget.state.aiModel); final apiKey = TextEditingController(); bool testingConnection = false; @override void dispose() { endpoint.dispose(); model.dispose(); apiKey.dispose(); super.dispose(); } @override Widget build(BuildContext context) => SpacedColumn( children: [ Text( '订阅版 ChatGPT / Gemini 不能直接作为 App API 使用。请使用自己的 API Key,或填写兼容 OpenAI 接口的 CLIProxyAPI 地址。AI 只提供建议,不会直接决定你的掌握程度。', style: TextStyle(fontSize: 12, color: AppColors.muted), ), DropdownButtonFormField( initialValue: widget.state.aiProvider, decoration: const InputDecoration( labelText: '服务类型', border: OutlineInputBorder(), ), items: const [ DropdownMenuItem( value: AiProviderType.mock, child: Text('内置练习模式(无需网络)'), ), DropdownMenuItem( value: AiProviderType.openAi, child: Text('OpenAI API'), ), DropdownMenuItem( value: AiProviderType.gemini, child: Text('Gemini API'), ), DropdownMenuItem( value: AiProviderType.compatible, child: Text('OpenAI 兼容 / CLIProxyAPI'), ), ], onChanged: _changeProvider, ), TextField( controller: endpoint, keyboardType: TextInputType.url, decoration: const InputDecoration( labelText: 'Base URL(可选)', hintText: 'OpenAI / 兼容: https://…/v1;Gemini: https://generativelanguage.googleapis.com/v1beta', border: OutlineInputBorder(), ), ), TextField( controller: model, decoration: const InputDecoration( labelText: '模型名称(可选)', hintText: '例如 gpt-4.1-mini', border: OutlineInputBorder(), ), ), TextField( controller: apiKey, obscureText: true, decoration: const InputDecoration( labelText: 'API Key(仅保存到设备安全存储)', border: OutlineInputBorder(), ), ), PrimaryButton(label: '保存服务设置', onPressed: _save), SecondaryButton( label: testingConnection ? '正在测试连接...' : '测试连接', onPressed: testingConnection ? null : _testConnection, ), SecondaryButton( label: '从配置文件重载 (ai_config.json)', onPressed: _reloadFromAsset, ), ], ); void _changeProvider(AiProviderType? value) { if (value == null) return; setState(() { widget.state.setAiProvider(value); if (value == AiProviderType.gemini && endpoint.text.trim().isEmpty) { endpoint.text = 'https://generativelanguage.googleapis.com/v1beta'; if (model.text.trim().isEmpty) model.text = 'gemini-2.5-flash'; } }); } Future _save() async { final messenger = ScaffoldMessenger.of(context); widget.state.saveAiConfiguration( endpoint: endpoint.text, model: model.text, ); if (apiKey.text.trim().isNotEmpty) { await AiService.instance.saveApiKey(apiKey.text); } messenger.showSnackBar(const SnackBar(content: Text('服务设置已保存。'))); } Future _testConnection() async { final messenger = ScaffoldMessenger.of(context); setState(() => testingConnection = true); final result = await AiService.instance.testConnection( provider: widget.state.aiProvider, endpoint: endpoint.text, model: model.text, explicitApiKey: apiKey.text, ); if (!mounted) return; setState(() => testingConnection = false); messenger.showSnackBar( SnackBar( content: Text(result.message), backgroundColor: result.ok ? AppColors.green : Colors.redAccent, duration: const Duration(seconds: 4), ), ); } Future _reloadFromAsset() async { final messenger = ScaffoldMessenger.of(context); final ok = await widget.state.reloadAiConfigFromAsset(); if (!mounted) return; if (ok) { setState(() { endpoint.text = widget.state.aiEndpoint; model.text = widget.state.aiModel; }); } messenger.showSnackBar( SnackBar( content: Text( ok ? '已从 assets/config/ai_config.json 载入配置。' : '未找到配置文件或解析失败。', ), ), ); } }