feat: 完善听力口语辅助状态记录、练习分块与复习抽查间隔机制

This commit is contained in:
shen
2026-09-23 18:33:22 -07:00
parent 6b42b7abc3
commit d740a9e19c
106 changed files with 2933 additions and 845 deletions
@@ -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('稍后继续')),
],
),