part of '../lesson_flow.dart'; class _IndependentStep extends StatefulWidget { const _IndependentStep({ required this.state, required this.segmentId, required this.keepRecording, required this.activity, required this.controller, required this.showHelp, required this.canContinue, required this.onChanged, required this.onNeedHelp, required this.onLookup, required this.onContinue, required this.onLater, }); final AppState state; final LessonActivity activity; final String segmentId; final bool keepRecording; final TextEditingController controller; final bool showHelp; final bool canContinue; final VoidCallback onChanged; final VoidCallback onNeedHelp; final VoidCallback onLookup; final void Function(bool spoken, String? recordingPath) onContinue; final VoidCallback onLater; @override State<_IndependentStep> createState() => _IndependentStepState(); } class _IndependentStepState extends State<_IndependentStep> with VoiceAnswerMixin<_IndependentStep> { bool usedVoice = false; bool transcriptEdited = false; String lastTranscript = ''; String? validationError; @override AppState get voiceState => widget.state; @override void dispose() { disposeVoiceAnswer(keepRecording: widget.keepRecording); super.dispose(); } void _submit() { if (!matchesSegmentIndependent(widget.segmentId, widget.controller.text)) { setState(() => validationError = '这次还没有用上本段要练的内容。查看帮助后补充一次。'); return; } widget.onContinue( usedVoice && !transcriptEdited, widget.keepRecording ? recordingPath : null, ); } Future _toggleMic() async { if (aiVoiceRecording) { await finishVoiceInput( onTranscript: (text) { widget.controller.text = text; usedVoice = true; lastTranscript = text; transcriptEdited = false; }, afterTranscribe: widget.onChanged, ); return; } if (listening) { await VoiceService.instance.stopListening(); if (mounted) setState(() => listening = false); return; } final ready = await VoiceService.instance.startListening( (text, _) { if (!mounted) return; setState(() { widget.controller.text = text; usedVoice = true; lastTranscript = text; transcriptEdited = false; }); widget.onChanged(); }, onStatus: (status) { if (mounted && (status == 'notListening' || status == 'done')) { setState(() => listening = false); } }, onError: (err) { if (mounted) { setState(() => listening = false); } }, ); if (!ready) { final recordStarted = await startVoiceInput(); if (recordStarted && mounted) { showVoiceMessage('已启动麦克风录音,说完后再次点击,AI 将自动转写为英文。'); } return; } if (mounted) setState(() => listening = ready); } @override Widget build(BuildContext context) => _LessonScaffold( step: 6, child: SpacedColumn( children: [ const Eyebrow('试着自己写 / 说一次 · 约 1 分钟'), Text('现在不看句框。', style: Theme.of(context).textTheme.headlineMedium), Text(widget.activity.independentPrompt), if (widget.showHelp) SectionCard( tint: AppColors.warm, child: Text( '帮助:${widget.activity.independentHelp}', style: TextStyle(color: AppColors.warmInk), ), ), TextField( controller: widget.controller, onChanged: (value) { if (usedVoice && value != lastTranscript) transcriptEdited = true; setState(() {}); widget.onChanged(); }, minLines: 2, maxLines: 4, decoration: InputDecoration( hintText: '输入完整英文句子', filled: true, fillColor: AppColors.surface, prefixIcon: IconButton( tooltip: listening ? '停止录音' : '语音输入', icon: Icon( listening ? Icons.stop_circle_outlined : Icons.mic_none, ), onPressed: _toggleMic, ), border: OutlineInputBorder(), ), ), SectionCard( tint: AppColors.surfaceMuted, child: SpacedColumn( spacing: 8, children: [ Text( widget.keepRecording ? '可录下这次尝试并保存在本机;不会发送给 AI。' : '可录下这次尝试并回听;离开本页后会自动删除。', ), RecordingControls( recording: recording, playing: playingRecording, hasRecording: recordingPath != null, onToggleRecording: toggleRecording, onPlay: playRecording, onDelete: deleteRecording, ), ], ), ), if (usedVoice) SectionCard( tint: transcriptEdited ? AppColors.warm : AppColors.softGreen, child: Text( transcriptEdited ? '你修改了设备转写:这次会按文字练习保存,不计口语练习。' : '这是设备转写。未修改并确认后,会保留为本次语音练习记录。', style: TextStyle( color: transcriptEdited ? AppColors.warmInk : AppColors.green, ), ), ), if (validationError != null) SectionCard( tint: AppColors.warm, child: Text( validationError!, style: const TextStyle(color: AppColors.warmInk), ), ), if (!widget.showHelp) Wrap( spacing: 8, children: [ TextButton( onPressed: widget.onNeedHelp, child: const Text('需要帮助'), ), TextButton( onPressed: widget.onLookup, child: const Text('查词或短语'), ), ], ) else TextButton(onPressed: widget.onLookup, child: const Text('查词或短语')), PrimaryButton( label: widget.showHelp ? '带帮助完成' : '独立完成', onPressed: widget.canContinue ? _submit : null, ), TextButton(onPressed: widget.onLater, child: const Text('稍后继续')), ], ), ); }