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:
co-authored by
Claude Opus 4.8
parent
9ab08c25ef
commit
febfd30f49
@@ -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]}',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kouyu_english/core/a0_core.dart';
|
||||
|
||||
import '../tool/courses/course_validator.dart';
|
||||
|
||||
void main() {
|
||||
test('tokenize expands contractions and skips digits', () {
|
||||
expect(tokenize("I'm Tom. Can't pay 15 dollars at 7 a.m.; let's go"), [
|
||||
'i', 'am', 'tom', 'can', 'not', 'pay', 'dollars', 'at', 'am', 'let',
|
||||
'us', 'go',
|
||||
]);
|
||||
});
|
||||
|
||||
test('match groups use AND across groups and OR inside a group', () {
|
||||
expect(satisfiesMatch("Sorry, I don't understand.", [
|
||||
['do not understand'],
|
||||
]), isTrue);
|
||||
expect(satisfiesMatch('twelvee', [
|
||||
['twelve'],
|
||||
]), isFalse);
|
||||
expect(satisfiesMatch('What is this word?', [
|
||||
['what does', 'what is'],
|
||||
['mean'],
|
||||
]), isFalse);
|
||||
});
|
||||
|
||||
test('course packs pass validation', () {
|
||||
final report = CourseValidator(
|
||||
projectRoot: Directory.current,
|
||||
a0Texts: [
|
||||
for (final item in a0CoreItems.values)
|
||||
item.replaceAll(RegExp(r'\[[^\]]*\]'), ''),
|
||||
for (final sentence in a0DictationSentences.values) sentence.sentence,
|
||||
],
|
||||
).validate();
|
||||
expect(report.errors.map((issue) => '$issue'), isEmpty);
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kouyu_english/core/a0_core.dart';
|
||||
import 'package:kouyu_english/core/models.dart';
|
||||
import 'package:kouyu_english/core/review_feedback.dart';
|
||||
|
||||
@@ -99,11 +100,35 @@ void main() {
|
||||
);
|
||||
// Multi-space on word item
|
||||
expect(
|
||||
ReviewFeedback.check(
|
||||
review('A0-W08', 'three'),
|
||||
' THREE ',
|
||||
).complete,
|
||||
ReviewFeedback.check(review('A0-W08', 'three'), ' THREE ').complete,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('a dictation review needs the whole sentence that was played', () {
|
||||
final item = review('A0-W24', 'water').copyWith(
|
||||
skill: dictationSkill,
|
||||
prompt: coreReviewVariant('A0-W24', 1).prompt,
|
||||
);
|
||||
expect(ReviewFeedback.check(item, 'water').complete, isFalse);
|
||||
expect(ReviewFeedback.check(item, 'I like watter.').complete, isFalse);
|
||||
expect(ReviewFeedback.check(item, 'i like water').complete, isTrue);
|
||||
});
|
||||
|
||||
test('offline review variants rotate recall, dictation and speaking', () {
|
||||
expect(coreReviewVariant('A0-P12', 0).skill, '回忆表达');
|
||||
expect(coreReviewVariant('A0-P12', 1).skill, dictationSkill);
|
||||
expect(coreReviewVariant('A0-P12', 2).skill, spokenRecallSkill);
|
||||
expect(coreReviewVariant('A0-P12', 3).skill, '回忆表达');
|
||||
for (final id in a0CoreItems.keys) {
|
||||
final sentence = a0DictationSentences[id];
|
||||
expect(sentence, isNotNull, reason: id);
|
||||
// Each dictation sentence really contains the item it checks.
|
||||
expect(
|
||||
coreItemUsedIn(id, sentence!.sentence, word: a0CoreItems[id]),
|
||||
isTrue,
|
||||
reason: id,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:kouyu_english/core/speech_compare.dart';
|
||||
|
||||
void main() {
|
||||
test('a matching transcript ignores case, punctuation and contractions', () {
|
||||
final result = compareSpoken(
|
||||
'Sorry, please say that again. I like water.',
|
||||
'sorry please say that again i like water',
|
||||
);
|
||||
expect(result.matches, isTrue);
|
||||
expect(result.heardCount, result.total);
|
||||
|
||||
expect(
|
||||
compareSpoken('I’m from Hong Kong.', 'I am from Hong Kong').matches,
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
compareSpoken('It’s three o’clock.', "it's three o'clock").matches,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('marks the words that were not heard, keeping word order', () {
|
||||
final result = compareSpoken('I like tea. Do you like tea?', 'I like tea');
|
||||
expect(result.matches, isFalse);
|
||||
expect(result.missedWords, ['Do', 'you', 'like', 'tea?']);
|
||||
expect(result.close, isFalse);
|
||||
|
||||
final extra = compareSpoken('I like water.', 'I like water very much');
|
||||
expect(extra.heardCount, extra.total);
|
||||
expect(extra.extraWords, ['very', 'much']);
|
||||
expect(extra.matches, isFalse);
|
||||
});
|
||||
|
||||
test('digits and joined letters line up with the model sentence', () {
|
||||
expect(
|
||||
compareSpoken(
|
||||
'My number is one-three-eight.',
|
||||
'My number is 138',
|
||||
).matches,
|
||||
isTrue,
|
||||
);
|
||||
expect(compareSpoken('S H E N', 'Shen').matches, isTrue);
|
||||
expect(compareSpoken('S H E N', 'S H E M').missedWords, ['N']);
|
||||
});
|
||||
}
|
||||
@@ -28,6 +28,13 @@ void main() {
|
||||
'a0-10',
|
||||
"I'm Alex. I'm from Beijing. I like tea.",
|
||||
).complete,
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
WritingFeedback.check(
|
||||
'a0-10',
|
||||
"I'm Alex. I'm from Beijing. I like water.",
|
||||
).complete,
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
@@ -39,10 +46,7 @@ void main() {
|
||||
isTrue,
|
||||
);
|
||||
// a0-01 with curly quote and multiple spaces
|
||||
expect(
|
||||
WritingFeedback.check('a0-01', 'HI I’M MIA').complete,
|
||||
isTrue,
|
||||
);
|
||||
expect(WritingFeedback.check('a0-01', 'HI I’M MIA').complete, isTrue);
|
||||
// a0-07 with multiple spaces in phrase
|
||||
expect(
|
||||
WritingFeedback.check('a0-07', 'THIS IS MY MOTHER').complete,
|
||||
|
||||
Reference in New Issue
Block a user