feat: A1 短材料复现本单元理解词,并加入黑夜模式

课程内容
- 重写全部 21 个 A1 单元的听读材料,把本单元理解词织进听/读文本,
  单元内理解词复现率从约 20% 提升到约 77%(各单元 56–96%)。
- 修正 U01 房间号与机场大巴同为 thirty 的撞车(改为 17/30/40)。
- 校验器容差按级别读取(A1 为 8%),materials 覆盖率、字数、
  选项子串等校验全部通过;course_content_test 通过。

黑夜模式
- app_theme 拆分明/暗两套调色板,AppColors 随亮度切换;
  main 用 theme/darkTheme/themeMode + builder 镜像已解析亮度;
  主题偏好持久化到快照;进度页新增“外观主题”切换。

文档
- COURSE-PACK-JSON.md 更新 A1 词池覆盖(840/933)与材料复现约定。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-18 11:16:09 +09:00
co-authored by Claude Opus 4.8
parent 9ab08c25ef
commit febfd30f49
93 changed files with 72676 additions and 274 deletions
+91 -3
View File
@@ -4,6 +4,7 @@ import 'package:kouyu_english/core/a0_core.dart';
import 'package:kouyu_english/core/assessment_bank.dart';
import 'package:kouyu_english/core/generated_content.dart';
import 'package:kouyu_english/core/models.dart';
import 'package:kouyu_english/core/review_feedback.dart';
import 'package:kouyu_english/core/seed_courses.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -52,13 +53,15 @@ void main() {
final item = state.dueReviews.single;
expect(a0CoreItems, contains(item.id));
expect(item.variantIndex, 1);
expect(item.prompt, contains('情境'));
// The first rotation turns the written recall into dictation.
expect(item.skill, dictationSkill);
expect(item.prompt, contains(''));
},
);
test('an approved AI variant remains tied to its original review item', () {
final state = AppState();
state.reviewQueue.add(dueItem());
state.reviewQueue.add(dueItem().copyWith(skill: dictationSkill));
state.applyGeneratedReviewVariant(
const GeneratedReviewVariant(
variantId: 'ai-1',
@@ -70,6 +73,7 @@ void main() {
expect(state.reviewQueue.single.id, 'A0-P12');
expect(state.reviewQueue.single.isAiGenerated, isTrue);
expect(state.reviewQueue.single.skill, isNot(dictationSkill));
});
test(
@@ -134,6 +138,17 @@ void main() {
createdAt: DateTime.now().subtract(const Duration(days: 20)),
),
);
state.attemptEvidence.add(
AttemptEvidence(
id: 'scene-use',
itemId: item.id,
taskId: 'dialogue-scene-a0-meet-0',
skill: '情境使用',
inputMode: 'text',
outcome: EvidenceKind.independentSuccess,
createdAt: DateTime.now().subtract(const Duration(days: 19)),
),
);
for (var index = 0; index < 4; index++) {
final current = state.reviewQueue.firstWhere(
@@ -197,6 +212,22 @@ void main() {
);
}
// Reworded reviews alone only show recall, never use in a situation.
state.rebuildMasteryFromEvidence();
expect(state.mastery['A0-P12']!.checkpoint, 4);
expect(state.mastery['A0-P12']!.status, MasteryStatus.recall);
state.attemptEvidence.add(
AttemptEvidence(
id: 'scene-use',
itemId: 'A0-P12',
taskId: 'dialogue-scene-a0-meet-1',
skill: '情境使用',
inputMode: 'text',
outcome: EvidenceKind.independentSuccess,
createdAt: base,
),
);
state.rebuildMasteryFromEvidence();
expect(state.mastery['A0-P12']!.checkpoint, 4);
@@ -946,10 +977,17 @@ void main() {
expect(
matchesSegmentIndependent(
'a0-10-a',
"I'm Mia. I'm from Guangzhou. I like tea. Do you like tea?",
"Please say that again. I'm Mia. I like water. Do you like tea?",
),
isTrue,
);
expect(
matchesSegmentIndependent(
'a0-10-a',
"I'm Mia. I'm from Guangzhou. I like tea. Do you like tea?",
),
isFalse,
);
});
test('each free scene keeps its own daily recap', () {
@@ -1370,4 +1408,54 @@ void main() {
expect(restored.dialogueDraft!.turns, hasLength(2));
expect(restored.dialogueDraft!.turns.last.isLearner, isTrue);
});
test('a spoken review answer keeps its transcript and input mode', () {
final state = AppState();
state.reviewQueue.add(dueItem());
state.completeReview(
state.reviewQueue.single,
assisted: false,
rawAnswer: "I'm from Hong Kong.",
inputMode: 'speechToText',
originalTranscript: "I'm from Hong Kong.",
);
final spoken = state.attemptEvidence.last;
expect(spoken.inputMode, 'speechToText');
expect(spoken.transcriptConfirmed, isTrue);
expect(spoken.transcriptEdited, isFalse);
final typed = AppState();
typed.reviewQueue.add(dueItem());
typed.completeReview(
typed.reviewQueue.single,
assisted: false,
rawAnswer: "I'm from Guangzhou.",
originalTranscript: "I'm from one show.",
);
expect(typed.attemptEvidence.last.inputMode, 'text');
expect(typed.attemptEvidence.last.transcriptEdited, isTrue);
});
test('each lesson practises the core items it targets', () {
for (final entry in a0TargetItemIdsByLesson.entries) {
final segments = lessonById(entry.key).segments;
final text = [
for (final segment in segments) ...[
activityBySegmentId(segment.id, entry.key).listening,
activityBySegmentId(segment.id, entry.key).speaking,
activityBySegmentId(segment.id, entry.key).reading,
activityBySegmentId(segment.id, entry.key).writingExample,
for (final item in vocabularyBySegmentId(segment.id, entry.key))
'${item.word}\n${item.example}',
],
].join('\n');
for (final id in entry.value) {
expect(
coreItemUsedIn(id, text, word: a0CoreItems[id]),
isTrue,
reason: '${entry.key}$id ${a0CoreItems[id]}',
);
}
}
});
}