feat: 完善听力口语辅助状态记录、练习分块与复习抽查间隔机制
This commit is contained in:
@@ -44,11 +44,7 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
final independentController = TextEditingController();
|
||||
int selectedAnswer = -1;
|
||||
|
||||
/// Set once a wrong meaning is picked, so only a first-try choice counts as
|
||||
/// recognition evidence.
|
||||
bool listeningMissed = false;
|
||||
int previewIndex = 0;
|
||||
bool listeningAudioPlayed = false;
|
||||
bool showWritingHelp = true;
|
||||
bool showIndependentHelp = false;
|
||||
LessonSegment get segment {
|
||||
@@ -56,11 +52,9 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
return lesson.segments[widget.state.activeSegmentIndexFor(lesson.id)];
|
||||
}
|
||||
|
||||
LessonActivity get activity =>
|
||||
activityBySegmentId(segment.id);
|
||||
LessonActivity get activity => activityBySegmentId(segment.id);
|
||||
|
||||
List<VocabularyItem> get previewItems =>
|
||||
vocabularyBySegmentId(segment.id);
|
||||
List<VocabularyItem> get previewItems => vocabularyBySegmentId(segment.id);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -122,17 +116,21 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
options: listeningOptions,
|
||||
correctAnswer: activity.answers.first,
|
||||
selectedAnswer: selectedAnswer,
|
||||
audioPlayed: listeningAudioPlayed,
|
||||
missed: listeningMissed,
|
||||
onSelected: (value) => setState(() {
|
||||
selectedAnswer = value;
|
||||
audioPlayed: widget.state.listeningAudioPlayed,
|
||||
audioUnavailable: widget.state.listeningAudioUnavailable,
|
||||
support: widget.state.listeningSupport,
|
||||
missed: widget.state.listeningAttempts > 0,
|
||||
onSelected: (value) {
|
||||
setState(() => selectedAnswer = value);
|
||||
if (listeningOptions[value] != activity.answers.first) {
|
||||
listeningMissed = true;
|
||||
widget.state.recordListeningMiss();
|
||||
}
|
||||
}),
|
||||
onPlayed: () => setState(() => listeningAudioPlayed = true),
|
||||
},
|
||||
onPlayed: () => widget.state.recordListeningPlayback(),
|
||||
onAudioFailed: widget.state.recordListeningAudioUnavailable,
|
||||
onSlowPlayed: () => widget.state.recordListeningPlayback(slow: true),
|
||||
onLookup: () {
|
||||
setState(() => listeningMissed = true);
|
||||
widget.state.revealListeningTranscript();
|
||||
showLexiconLookup(
|
||||
context,
|
||||
state: widget.state,
|
||||
@@ -142,8 +140,7 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
onContinue:
|
||||
selectedAnswer >= 0 &&
|
||||
listeningOptions[selectedAnswer] == activity.answers.first
|
||||
? () =>
|
||||
widget.state.completeListening(recognized: !listeningMissed)
|
||||
? () => widget.state.completeListening(recognized: true)
|
||||
: null,
|
||||
);
|
||||
case LessonStep.speaking:
|
||||
@@ -152,8 +149,13 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
segmentId: segment.id,
|
||||
text: activity.speaking,
|
||||
tip: activity.speakingTip,
|
||||
chunks: activity.speakingChunks,
|
||||
examples: activity.speakingExamples,
|
||||
buildIndex: widget.state.sentenceBuildIndex,
|
||||
onBuildProgress: widget.state.recordSentenceBuildProgress,
|
||||
keepRecording: widget.state.keepRecordings,
|
||||
onContinue: widget.state.completeSpeaking,
|
||||
onContinue: (practiced) =>
|
||||
widget.state.completeSpeaking(practiced: practiced),
|
||||
);
|
||||
case LessonStep.reading:
|
||||
content = _ReadingStep(
|
||||
@@ -191,8 +193,11 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
},
|
||||
onToggleHelp: () =>
|
||||
setState(() => showWritingHelp = !showWritingHelp),
|
||||
onContinue: (usedAiFeedback) => widget.state.completeWriting(
|
||||
assisted: showWritingHelp || usedAiFeedback,
|
||||
onContinue: (verified) => widget.state.completeWriting(
|
||||
// The example is visible when this guided step opens. Hiding it
|
||||
// later cannot turn the same answer into an independent attempt.
|
||||
assisted: true,
|
||||
verified: verified,
|
||||
rawAnswer: writingController.text.trim(),
|
||||
),
|
||||
);
|
||||
@@ -221,9 +226,10 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
initialText: activity.independentPrompt,
|
||||
);
|
||||
},
|
||||
onContinue: (spoken, recordingPath, aiCorrected) =>
|
||||
onContinue: (spoken, recordingPath, aiCorrected, verified) =>
|
||||
widget.state.completeIndependentAttempt(
|
||||
assisted: showIndependentHelp || aiCorrected,
|
||||
verified: verified,
|
||||
spoken: spoken,
|
||||
rawAnswer: independentController.text.trim(),
|
||||
recordingPath: recordingPath,
|
||||
@@ -234,6 +240,7 @@ class _LessonFlowState extends State<LessonFlow> {
|
||||
case LessonStep.complete:
|
||||
content = _CompletionStep(
|
||||
assisted: widget.state.independentAttemptAssisted,
|
||||
pending: widget.state.independentAttemptPending,
|
||||
isFinalSegment:
|
||||
widget.state.activeSegmentIndexFor(lesson.id) ==
|
||||
lesson.segments.length - 1,
|
||||
@@ -343,11 +350,13 @@ class _DialoguePendingStep extends StatelessWidget {
|
||||
class _CompletionStep extends StatelessWidget {
|
||||
const _CompletionStep({
|
||||
required this.assisted,
|
||||
required this.pending,
|
||||
required this.isFinalSegment,
|
||||
required this.nextSegmentNumber,
|
||||
required this.onFinish,
|
||||
});
|
||||
final bool assisted;
|
||||
final bool pending;
|
||||
final bool isFinalSegment;
|
||||
final int nextSegmentNumber;
|
||||
final VoidCallback onFinish;
|
||||
@@ -362,7 +371,13 @@ class _CompletionStep extends StatelessWidget {
|
||||
isFinalSegment ? '你完成了这节课的练习。' : '你完成了当前小段的练习。',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
Text(assisted ? '独立尝试使用了帮助,系统会安排不同题再练一次。' : '这次独立尝试已记录,后续会在不同情境中复练。'),
|
||||
Text(
|
||||
pending
|
||||
? '练习已保存,但答案未经 AI 核验,不计入独立掌握。后续会安排复练。'
|
||||
: assisted
|
||||
? '独立尝试使用了帮助,系统会安排不同题再练一次。'
|
||||
: '这次独立尝试已记录,后续会在不同情境中复练。',
|
||||
),
|
||||
if (!isFinalSegment) Text('回到首页后,下次从第 $nextSegmentNumber 段继续。'),
|
||||
PrimaryButton(label: '回到首页', onPressed: onFinish),
|
||||
],
|
||||
@@ -371,20 +386,28 @@ class _CompletionStep extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _AudioRow extends StatelessWidget {
|
||||
const _AudioRow({required this.label, this.speech, this.onPlayed});
|
||||
const _AudioRow({
|
||||
required this.label,
|
||||
this.speech,
|
||||
this.onPlayed,
|
||||
this.onFailed,
|
||||
this.showSlow = true,
|
||||
});
|
||||
final String label;
|
||||
final String? speech;
|
||||
final VoidCallback? onPlayed;
|
||||
final VoidCallback? onFailed;
|
||||
final bool showSlow;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(
|
||||
children: [
|
||||
IconButton.filled(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await VoiceService.instance.speak(speech ?? label);
|
||||
} finally {
|
||||
if (await VoiceService.instance.speak(speech ?? label)) {
|
||||
onPlayed?.call();
|
||||
} else {
|
||||
onFailed?.call();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
@@ -393,10 +416,10 @@ class _AudioRow extends StatelessWidget {
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
try {
|
||||
await VoiceService.instance.speak(speech ?? label);
|
||||
} finally {
|
||||
if (await VoiceService.instance.speak(speech ?? label)) {
|
||||
onPlayed?.call();
|
||||
} else {
|
||||
onFailed?.call();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
@@ -405,16 +428,18 @@ class _AudioRow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
await VoiceService.instance.speak(speech ?? label, slow: true);
|
||||
} finally {
|
||||
onPlayed?.call();
|
||||
}
|
||||
},
|
||||
child: const Text('慢速'),
|
||||
),
|
||||
if (showSlow)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (await VoiceService.instance.speak(
|
||||
speech ?? label,
|
||||
slow: true,
|
||||
)) {
|
||||
onPlayed?.call();
|
||||
}
|
||||
},
|
||||
child: const Text('慢速'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user