feat: 重做阅读找答案题与 AI 情境对话,并归拢本地识别、查词等既有改动
阅读"在对话里找到答案": - 16 道题全部重写,干扰项真实出现在对话里,靠说话人归属或否定句才能作答 - 选项按题目内容确定性打乱,答案不再固定排第一;听力环节同样处理 - 去掉超纲干扰项、重复题干,收紧自由作答匹配(过去单个字母也能判对) AI 情境对话: - 提示词区分"AI 这一句要做什么"与"学习者随后要完成什么",并下发已教词句清单 - JSON 只强制 reply,translation/feedback 可选;不再索要用不上的 slots/evidence - AI 不可用时页面明确提示当前回复来自内置示范脚本 - 删掉按 stage 下标猜中文翻译的兜底,避免译文与英文对不上 - 整课对话改用逐轮必需表达校验,替换"关键词沾边就算过";修正自由场景正则误伤 - 总结的"完成任务"按实际通过的轮次生成;模型点评只在结束页呈现一次 - 自由场景支持草稿续练(独立存储槽);修正回答轮数文案与永不解锁的场景标注 同时提交此前工作区中累积的改动:SenseVoice 本地识别、查词/句型解析卡、 复习与测评页调整等,并补充对话校验、选项分布和句子解析的测试。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,10 @@ class AppState extends ChangeNotifier {
|
||||
final List<AssessmentRecord> assessments = [];
|
||||
AssessmentDraft? assessmentDraft;
|
||||
DialogueDraft? dialogueDraft;
|
||||
|
||||
/// The free "初次见面" scene keeps its own slot, so resuming a scene never
|
||||
/// overwrites an unfinished lesson dialogue.
|
||||
DialogueDraft? sceneDialogueDraft;
|
||||
LessonStep lessonStep = LessonStep.preview;
|
||||
int previewIndex = 0;
|
||||
bool lessonListeningComplete = false;
|
||||
@@ -80,6 +84,7 @@ class AppState extends ChangeNotifier {
|
||||
String? adaptiveLessonDraftRecordingPath;
|
||||
final Set<String> reportedAdaptiveLessonIds = {};
|
||||
final Map<String, TemporaryLexiconEntry> temporaryLexicon = {};
|
||||
final Map<String, SentenceAnalysisResult> sentenceAnalyses = {};
|
||||
|
||||
final List<ReviewItem> reviewQueue = [];
|
||||
final List<AttemptEvidence> attemptEvidence = [];
|
||||
@@ -255,6 +260,17 @@ class AppState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void saveSceneDialogueDraft(DialogueDraft draft) {
|
||||
sceneDialogueDraft = draft;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearSceneDialogueDraft() {
|
||||
if (sceneDialogueDraft == null) return;
|
||||
sceneDialogueDraft = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int get usableMasteryCount => mastery.values
|
||||
.where(
|
||||
(item) =>
|
||||
@@ -407,6 +423,24 @@ class AppState extends ChangeNotifier {
|
||||
),
|
||||
);
|
||||
}
|
||||
final savedSentenceAnalyses = data['sentenceAnalyses'] as List<dynamic>?;
|
||||
if (savedSentenceAnalyses != null) {
|
||||
sentenceAnalyses
|
||||
..clear()
|
||||
..addEntries(
|
||||
savedSentenceAnalyses
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((item) {
|
||||
final query = item['query'] as String? ?? '';
|
||||
final payload = item['payload'] as Map<String, dynamic>? ?? {};
|
||||
return MapEntry(
|
||||
_normalizeLexiconQuery(query),
|
||||
SentenceAnalysisResult.fromJson(payload),
|
||||
);
|
||||
})
|
||||
.where((entry) => entry.key.isNotEmpty && entry.value.translation.isNotEmpty),
|
||||
);
|
||||
}
|
||||
aiProvider = _enumValue(
|
||||
AiProviderType.values,
|
||||
data['aiProvider'] as String?,
|
||||
@@ -580,26 +614,51 @@ class AppState extends ChangeNotifier {
|
||||
.map((key, value) => MapEntry(key, value == true)),
|
||||
);
|
||||
}
|
||||
final savedDialogue = data['dialogueDraft'] as Map<String, dynamic>?;
|
||||
if (savedDialogue != null) {
|
||||
dialogueDraft = DialogueDraft(
|
||||
lessonId: savedDialogue['lessonId'] as String,
|
||||
stage: savedDialogue['stage'] as int? ?? 0,
|
||||
usedHelp: savedDialogue['usedHelp'] as bool? ?? false,
|
||||
turns: (savedDialogue['turns'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(
|
||||
(turn) => DialogueTurn(
|
||||
text: turn['text'] as String,
|
||||
isLearner: turn['isLearner'] as bool? ?? false,
|
||||
translation: turn['translation'] as String?,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
dialogueDraft = _dialogueDraftFromJson(
|
||||
data['dialogueDraft'] as Map<String, dynamic>?,
|
||||
);
|
||||
sceneDialogueDraft = _dialogueDraftFromJson(
|
||||
data['sceneDialogueDraft'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
DialogueDraft? _dialogueDraftFromJson(Map<String, dynamic>? saved) {
|
||||
if (saved == null || saved['lessonId'] is! String) return null;
|
||||
return DialogueDraft(
|
||||
lessonId: saved['lessonId'] as String,
|
||||
stage: saved['stage'] as int? ?? 0,
|
||||
usedHelp: saved['usedHelp'] as bool? ?? false,
|
||||
turns: (saved['turns'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(
|
||||
(turn) => DialogueTurn(
|
||||
text: turn['text'] as String,
|
||||
isLearner: turn['isLearner'] as bool? ?? false,
|
||||
translation: turn['translation'] as String?,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic>? _dialogueDraftToJson(DialogueDraft? draft) =>
|
||||
draft == null
|
||||
? null
|
||||
: {
|
||||
'lessonId': draft.lessonId,
|
||||
'stage': draft.stage,
|
||||
'usedHelp': draft.usedHelp,
|
||||
'turns': draft.turns
|
||||
.map(
|
||||
(turn) => {
|
||||
'text': turn.text,
|
||||
'isLearner': turn.isLearner,
|
||||
'translation': turn.translation,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
|
||||
T _enumValue<T extends Enum>(List<T> values, String? name, T fallback) =>
|
||||
values.where((value) => value.name == name).firstOrNull ?? fallback;
|
||||
|
||||
@@ -663,6 +722,17 @@ class AppState extends ChangeNotifier {
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
'sentenceAnalyses': sentenceAnalyses.entries
|
||||
.map(
|
||||
(entry) => {
|
||||
'query': entry.key,
|
||||
'payload': entry.value.toJson(),
|
||||
'provider': entry.value.provider,
|
||||
'model': entry.value.model,
|
||||
'createdAt': entry.value.createdAt.toIso8601String(),
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
'aiProvider': aiProvider.name,
|
||||
'lessonStep': lessonStep.name,
|
||||
'previewIndex': previewIndex,
|
||||
@@ -754,22 +824,8 @@ class AppState extends ChangeNotifier {
|
||||
'taskIndex': assessmentDraft!.taskIndex,
|
||||
'results': assessmentDraft!.results,
|
||||
},
|
||||
'dialogueDraft': dialogueDraft == null
|
||||
? null
|
||||
: {
|
||||
'lessonId': dialogueDraft!.lessonId,
|
||||
'stage': dialogueDraft!.stage,
|
||||
'usedHelp': dialogueDraft!.usedHelp,
|
||||
'turns': dialogueDraft!.turns
|
||||
.map(
|
||||
(turn) => {
|
||||
'text': turn.text,
|
||||
'isLearner': turn.isLearner,
|
||||
'translation': turn.translation,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
},
|
||||
'dialogueDraft': _dialogueDraftToJson(dialogueDraft),
|
||||
'sceneDialogueDraft': _dialogueDraftToJson(sceneDialogueDraft),
|
||||
}),
|
||||
).catchError((_) => false).whenComplete(() {
|
||||
_writing = false;
|
||||
@@ -815,6 +871,46 @@ class AppState extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
SentenceAnalysisResult? sentenceAnalysisFor(String query) =>
|
||||
sentenceAnalyses[_normalizeLexiconQuery(query)];
|
||||
|
||||
void cacheSentenceAnalysis(SentenceAnalysisResult result) {
|
||||
final key = _normalizeLexiconQuery(result.originalText);
|
||||
if (key.isEmpty || result.translation.trim().isEmpty) return;
|
||||
sentenceAnalyses[key] = result;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void removeSentenceAnalysis(String query) {
|
||||
if (sentenceAnalyses.remove(_normalizeLexiconQuery(query)) != null) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void addPhraseToReview({
|
||||
required String phrase,
|
||||
required String meaning,
|
||||
String? ipa,
|
||||
String? usageNote,
|
||||
required String contextSentence,
|
||||
}) {
|
||||
final cleanPhrase = phrase.trim();
|
||||
if (cleanPhrase.isEmpty) return;
|
||||
final id = 'phrase_${cleanPhrase.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '_')}';
|
||||
final fullMeaning = usageNote != null && usageNote.trim().isNotEmpty
|
||||
? '$meaning ($usageNote)'
|
||||
: meaning;
|
||||
final item = VocabularyItem(
|
||||
id: id,
|
||||
word: cleanPhrase,
|
||||
meaning: fullMeaning,
|
||||
example: contextSentence.trim(),
|
||||
exampleMeaning: meaning,
|
||||
ipa: ipa,
|
||||
);
|
||||
addSavedWord(item);
|
||||
}
|
||||
|
||||
void finishOnboarding() {
|
||||
onboardingComplete = true;
|
||||
notifyListeners();
|
||||
@@ -1631,6 +1727,7 @@ class AppState extends ChangeNotifier {
|
||||
assessments.clear();
|
||||
assessmentDraft = null;
|
||||
dialogueDraft = null;
|
||||
sceneDialogueDraft = null;
|
||||
lessonStep = LessonStep.preview;
|
||||
previewIndex = 0;
|
||||
lessonListeningComplete = false;
|
||||
|
||||
Reference in New Issue
Block a user