Files
English/kouyu_english/lib/features/profile/ai_service_page.dart
T

172 lines
5.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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://…/v1Gemini 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 载入配置。' : '未找到配置文件或解析失败。',
),
),
);
}
}