172 lines
5.4 KiB
Dart
172 lines
5.4 KiB
Dart
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 载入配置。' : '未找到配置文件或解析失败。',
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|