整个课程和数据UI重新设计版本
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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<AiServiceView> createState() => _AiServiceViewState();
|
||||
}
|
||||
|
||||
class _AiServiceViewState extends State<AiServiceView> {
|
||||
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<AiProviderType>(
|
||||
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<void> _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<void> _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<void> _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 载入配置。' : '未找到配置文件或解析失败。',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user