feat: 完善听力口语辅助状态记录、练习分块与复习抽查间隔机制
This commit is contained in:
@@ -27,7 +27,12 @@ class _IndependentStep extends StatefulWidget {
|
||||
final VoidCallback onChanged;
|
||||
final VoidCallback onNeedHelp;
|
||||
final VoidCallback onLookup;
|
||||
final void Function(bool spoken, String? recordingPath, bool aiCorrected)
|
||||
final void Function(
|
||||
bool spoken,
|
||||
String? recordingPath,
|
||||
bool aiCorrected,
|
||||
bool verified,
|
||||
)
|
||||
onContinue;
|
||||
final VoidCallback onLater;
|
||||
|
||||
@@ -57,7 +62,8 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
Future<void> _submit() async {
|
||||
if (!widget.canContinue || checkingWithAi) return;
|
||||
final shortfall = independentShortfall(
|
||||
widget.segmentId,
|
||||
widget.controller.text,
|
||||
@@ -66,12 +72,20 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
setState(() => validationError = shortfall);
|
||||
return;
|
||||
}
|
||||
if (widget.state.aiProvider == AiProviderType.mock) {
|
||||
_finish(verified: false);
|
||||
return;
|
||||
}
|
||||
await _checkWithAi();
|
||||
if (!mounted) return;
|
||||
if (aiCheck?.verdict == 'accepted') _finish(verified: true);
|
||||
}
|
||||
|
||||
void _finish({required bool verified}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
aiCheck?.verdict == 'accepted'
|
||||
? '通过:用上了本段要练的表达,AI 也没发现拼写或语法问题。'
|
||||
: '通过:用上了本段要练的表达。',
|
||||
verified ? '通过:用上了本段要练的表达,AI 也没发现拼写或语法问题。' : '练习已保存;答案未经核验,不计入独立掌握。',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -79,6 +93,7 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
usedVoice && !transcriptEdited,
|
||||
widget.keepRecording ? recordingPath : null,
|
||||
aiCorrected,
|
||||
verified,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,13 +105,9 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
|
||||
Future<void> _checkWithAi() async {
|
||||
final answer = widget.controller.text.trim();
|
||||
if (answer.isEmpty || checkingWithAi) return;
|
||||
if (widget.state.aiProvider == AiProviderType.mock) {
|
||||
setState(() => aiCheckError = '请先在“我的”配置 AI 服务;本地检查仍可继续学习。');
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
checkingWithAi = true;
|
||||
aiCheck = null;
|
||||
aiCheckError = null;
|
||||
});
|
||||
final feedback = await AiService.instance.capabilities.evaluation
|
||||
@@ -119,9 +130,11 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
checkingWithAi = false;
|
||||
aiCheck = feedback;
|
||||
aiCheckError = feedback == null
|
||||
? '暂时无法获得 AI 检查结果。你的答案保留在这里,可稍后重试或直接提交。'
|
||||
? '暂时无法核验答案。可重试,或仅保存练习继续。'
|
||||
: feedback.verdict == 'uncertain'
|
||||
? 'AI 无法确定答案是否正确。可重试,或仅保存练习继续。'
|
||||
: null;
|
||||
if (feedback != null && feedback.verdict != 'accepted') {
|
||||
if (feedback?.verdict == 'rewrite') {
|
||||
aiCorrected = true;
|
||||
}
|
||||
});
|
||||
@@ -191,6 +204,11 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
const Eyebrow('试着自己写 / 说一次 · 约 1 分钟'),
|
||||
Text('现在不看句框。', style: Theme.of(context).textTheme.headlineMedium),
|
||||
Text(widget.activity.independentPrompt),
|
||||
if (widget.state.aiProvider == AiProviderType.mock)
|
||||
SectionCard(
|
||||
tint: AppColors.warm,
|
||||
child: const Text('尚未配置 AI 服务。本次可继续练习,但开放回答不会计入独立掌握。'),
|
||||
),
|
||||
if (widget.showHelp)
|
||||
SectionCard(
|
||||
tint: AppColors.warm,
|
||||
@@ -255,19 +273,6 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
),
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: widget.canContinue && !checkingWithAi
|
||||
? _checkWithAi
|
||||
: null,
|
||||
icon: checkingWithAi
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.spellcheck),
|
||||
label: Text(checkingWithAi ? '正在检查…' : '提交前 AI 检查语法和拼写(可选)'),
|
||||
),
|
||||
if (aiCheck != null)
|
||||
SectionCard(
|
||||
tint: aiCheck!.verdict == 'accepted'
|
||||
@@ -282,8 +287,10 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
Text('参考改正:${aiCheck!.suggestion}'),
|
||||
Text(
|
||||
aiCheck!.verdict == 'accepted'
|
||||
? 'AI 仅作参考;是否完成仍由本地检查判断。'
|
||||
: '看过改正后再提交,本次会记为带提示完成。',
|
||||
? '本地目标与 AI 检查均通过。'
|
||||
: aiCheck!.verdict == 'rewrite'
|
||||
? '修改后再次提交;看过改正,本次会记为辅助练习。'
|
||||
: '答案尚未核验,可重试或仅保存练习。',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.muted),
|
||||
),
|
||||
],
|
||||
@@ -322,9 +329,14 @@ class _IndependentStepState extends State<_IndependentStep>
|
||||
else
|
||||
TextButton(onPressed: widget.onLookup, child: const Text('查词或短语')),
|
||||
PrimaryButton(
|
||||
label: widget.showHelp || aiCorrected ? '带帮助完成' : '独立完成',
|
||||
onPressed: widget.canContinue ? _submit : null,
|
||||
label: checkingWithAi ? '正在自动检查…' : '提交独立尝试',
|
||||
onPressed: widget.canContinue && !checkingWithAi ? _submit : null,
|
||||
),
|
||||
if (aiCheckError != null)
|
||||
TextButton(
|
||||
onPressed: () => _finish(verified: false),
|
||||
child: const Text('未核验,保存练习并继续'),
|
||||
),
|
||||
TextButton(onPressed: widget.onLater, child: const Text('稍后继续')),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user