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
@@ -34,23 +34,18 @@ class _WritingStepState extends State<_WritingStep> {
String? aiFeedbackError;
bool requestingAiFeedback = false;
void _checkOrContinue() {
if (result?.complete == true) {
widget.onContinue(aiFeedback != null);
return;
}
setState(
() => result = WritingFeedback.check(
widget.segmentId,
widget.controller.text,
),
);
}
Future<void> _requestAiFeedback() async {
if (widget.controller.text.trim().isEmpty || requestingAiFeedback) return;
Future<void> _submit() async {
if (!widget.canContinue || requestingAiFeedback) return;
final answer = widget.controller.text.trim();
final local = WritingFeedback.check(widget.segmentId, answer);
setState(() {
result = local;
aiFeedback = null;
aiFeedbackError = null;
});
if (!local.complete) return;
if (widget.state.aiProvider == AiProviderType.mock) {
setState(() => aiFeedbackError = '请先在“我的”配置 AI 服务;本地检查仍可继续学习。');
widget.onContinue(false);
return;
}
setState(() {
@@ -64,19 +59,28 @@ class _WritingStepState extends State<_WritingStep> {
model: widget.state.aiModel,
lessonId: widget.lessonId,
taskPrompt: widget.activity.writingPrompt,
answer: widget.controller.text.trim(),
answer: answer,
level: lessonLevel(widget.lessonId),
);
if (!mounted) return;
if (!mounted || widget.controller.text.trim() != answer) {
if (mounted) setState(() => requestingAiFeedback = false);
return;
}
if (feedback?.verdict == 'accepted') {
widget.onContinue(true);
return;
}
setState(() {
requestingAiFeedback = false;
aiFeedback = feedback;
aiFeedbackError = feedback == null
? '暂时无法获得 AI 反馈。你的答案保留在这里,可稍后重试或继续本地练习。'
aiFeedbackError = feedback == null || feedback.verdict == 'uncertain'
? '答案暂时无法核验。可以重试,或仅保存这次练习继续学习。'
: null;
});
}
void _continueUnverified() => widget.onContinue(false);
@override
Widget build(BuildContext context) => _LessonScaffold(
step: 5,
@@ -87,6 +91,11 @@ class _WritingStepState extends State<_WritingStep> {
widget.activity.writingPrompt,
style: Theme.of(context).textTheme.headlineMedium,
),
if (widget.state.aiProvider == AiProviderType.mock)
SectionCard(
tint: AppColors.warm,
child: const Text('尚未配置 AI 服务。本次可继续练习,但开放回答不会计入独立掌握。'),
),
SectionCard(
tint: AppColors.surfaceMuted,
child: Text('小提示:${grammarNoteForSegment(widget.segmentId)}'),
@@ -126,19 +135,6 @@ class _WritingStepState extends State<_WritingStep> {
onPressed: widget.onToggleHelp,
child: Text(widget.showHelp ? '收起示例,自己试一次' : '需要帮助,查看示例'),
),
OutlinedButton.icon(
onPressed: widget.canContinue && !requestingAiFeedback
? _requestAiFeedback
: null,
icon: requestingAiFeedback
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.auto_awesome_outlined),
label: Text(requestingAiFeedback ? '正在获取反馈…' : '获取 AI 写作建议(可选)'),
),
if (aiFeedback != null)
SectionCard(
tint: aiFeedback!.verdict == 'accepted'
@@ -153,7 +149,9 @@ class _WritingStepState extends State<_WritingStep> {
if (aiFeedback!.suggestion != null)
Text('可参考改写:${aiFeedback!.suggestion}'),
Text(
'这是学习帮助;请按自己的意思重写后再检查,系统不会仅凭 AI 建议记为掌握。',
aiFeedback!.verdict == 'rewrite'
? '请按自己的意思修改,再提交检查。看过改正后,本次只算辅助练习。'
: '答案尚未核验;可以重试检查或仅保存练习。',
style: TextStyle(fontSize: 12, color: AppColors.muted),
),
],
@@ -167,20 +165,25 @@ class _WritingStepState extends State<_WritingStep> {
style: TextStyle(color: AppColors.warmInk),
),
),
if (result != null)
if (result != null && !result!.complete)
SectionCard(
tint: result!.complete ? AppColors.softGreen : AppColors.warm,
tint: AppColors.warm,
child: Text(
result!.message,
style: TextStyle(
color: result!.complete ? AppColors.green : AppColors.warmInk,
),
style: TextStyle(color: AppColors.warmInk),
),
),
PrimaryButton(
label: result?.complete == true ? '进入课程对话' : '检查句子',
onPressed: widget.canContinue ? _checkOrContinue : null,
label: requestingAiFeedback ? '正在自动检查…' : '提交并继续',
onPressed: widget.canContinue && !requestingAiFeedback
? _submit
: null,
),
if (aiFeedbackError != null)
TextButton(
onPressed: _continueUnverified,
child: const Text('未核验,保存练习并继续'),
),
],
),
);