整个课程和数据UI重新设计版本

This commit is contained in:
shenlei
2026-09-18 17:55:44 +09:00
parent febfd30f49
commit 0a4f97d105
154 changed files with 12366 additions and 4784 deletions
+37 -3
View File
@@ -1,6 +1,6 @@
import 'dart:convert';
import 'a0_core.dart';
import 'courses/courses.dart';
import 'models.dart';
/// A bounded AI-authored variation of an existing review item. It never
@@ -120,6 +120,40 @@ bool matchesAdaptiveLessonAnswer(GeneratedLessonTask task, String answer) {
return accepted || slots;
}
/// Explains in Chinese why [matchesAdaptiveLessonAnswer] rejects [answer],
/// or returns null when it accepts it. Listening answers only get a count so
/// the explanation does not replace listening again.
String? adaptiveAnswerShortfall(GeneratedLessonTask task, String answer) {
if (matchesAdaptiveLessonAnswer(task, answer)) return null;
final actual = _normalizeAnswer(answer);
if (actual.isEmpty) return '请先输入或说出答案。';
final spec = task.localAnswerSpec;
final forbidden = spec.forbiddenPhrases
.where((phrase) => _containsPhrase(actual, phrase))
.toList();
if (forbidden.isNotEmpty) {
return '这里不能用:${forbidden.join('、')}。';
}
final missing = spec.requiredAnyPhrases
.where(
(alternatives) =>
!alternatives.any((phrase) => _containsPhrase(actual, phrase)),
)
.toList();
if (task.skill == 'listening') {
return '还缺 ${missing.isEmpty ? 1 : missing.length} 处关键信息。再听一次,或慢放后补全。';
}
if (missing.isEmpty || answerSpecIsReferenceOnly(spec, task.answer)) {
return '和参考表达还不一致,检查拼写和漏掉的词。';
}
return '还缺:${missing.map((group) => group.take(3).join(' / ')).join(';')}。';
}
bool answerSpecIsReferenceOnly(GeneratedAnswerSpec spec, String answer) =>
spec.requiredAnyPhrases.length == 1 &&
spec.requiredAnyPhrases.single.length == 1 &&
spec.requiredAnyPhrases.single.single == answer;
String _normalizeAnswer(String value) => value
.toLowerCase()
.replaceAll('’', "'")
@@ -157,7 +191,7 @@ GeneratedReviewVariant? decodeGeneratedReviewVariant(
expectedAnswer.trim().isEmpty ||
expectedAnswer.length > 120 ||
targetItemId != expectedTargetItemId ||
!a0CoreItems.containsKey(targetItemId)) {
!isCoreItem(targetItemId)) {
return null;
}
return GeneratedReviewVariant(
@@ -231,7 +265,7 @@ GeneratedLesson? decodeGeneratedLesson(
rawTasks is! List ||
targets.length != 1 ||
targets.single != expectedTargetItemId ||
!a0CoreItems.containsKey(expectedTargetItemId) ||
!isCoreItem(expectedTargetItemId) ||
newItems.isNotEmpty ||
preview.isNotEmpty ||
chunks.isNotEmpty ||