feat: 增加AI配置文件与TTS自动朗读,修复二级页面返回按钮与音频播放

This commit is contained in:
shenlei
2026-09-15 19:11:39 +09:00
parent 37c86f7ecb
commit a717365c10
18 changed files with 1322 additions and 397 deletions
@@ -10,11 +10,26 @@ import '../../widgets/app_widgets.dart';
import '../../widgets/lexicon_lookup.dart';
class DialogueScenePage extends StatelessWidget {
const DialogueScenePage({super.key, required this.onStart});
const DialogueScenePage({
super.key,
required this.onStart,
this.onBack,
});
final VoidCallback onStart;
final VoidCallback? onBack;
@override
Widget build(BuildContext context) => AppPage(
appBar: onBack != null
? AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: "返回",
onPressed: onBack,
),
title: const Text("AI 情境对话"),
)
: null,
child: SpacedColumn(
children: [
const Eyebrow('按当前水平推荐'),
@@ -157,10 +172,17 @@ class _DialoguePageState extends State<DialoguePage> {
} else {
turns.add(DialogueTurn(text: script.prompts.first, isLearner: false));
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_playLatestAi(slow: false);
}
});
}
@override
void dispose() {
VoiceService.instance.stopSpeaking();
VoiceService.instance.stopListening();
VoiceService.instance.stopRecordingPlayback();
if (!widget.state.keepRecordings) {
VoiceService.instance.deleteRecording(recordingPath);
@@ -217,20 +239,21 @@ class _DialoguePageState extends State<DialoguePage> {
.toList(),
);
if (!mounted) return;
final replyText = aiResponse?.reply ??
(nextStage < script.prompts.length
? script.prompts[nextStage]
: 'Wonderful — nice meeting you!');
setState(() {
turns.add(
DialogueTurn(
text:
aiResponse?.reply ??
(nextStage < script.prompts.length
? script.prompts[nextStage]
: 'Wonderful — nice meeting you!'),
text: replyText,
isLearner: false,
),
);
waitingForReply = false;
});
_saveDraft();
VoiceService.instance.speak(replyText);
}
void _saveDraft() {
@@ -417,6 +440,11 @@ class _DialoguePageState extends State<DialoguePage> {
final finished = stage == script.prompts.length;
return AppPage(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: "返回",
onPressed: () => widget.onFinished(null),
),
title: Text(
'${widget.isLessonDialogue ? '课程对话' : '初次见面'} · ${finished ? 4 : stage + 1} / 4',
),
@@ -446,7 +474,37 @@ class _DialoguePageState extends State<DialoguePage> {
: AppColors.softGreen,
borderRadius: BorderRadius.circular(15),
),
child: LexiconText(turn.text, state: widget.state),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LexiconText(turn.text, state: widget.state),
if (!turn.isLearner) ...[
const SizedBox(height: 6),
GestureDetector(
onTap: () => VoiceService.instance.speak(turn.text),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.volume_up_outlined,
size: 16,
color: AppColors.green,
),
SizedBox(width: 4),
Text(
"播放",
style: TextStyle(
fontSize: 12,
color: AppColors.green,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
],
),
),
);
},
@@ -612,13 +670,24 @@ class DialogueSummaryPage extends StatelessWidget {
required this.onHome,
required this.onLesson,
required this.onRetry,
this.onBack,
});
final DialogueSummaryData summary;
final VoidCallback onHome;
final VoidCallback onLesson;
final VoidCallback onRetry;
final VoidCallback? onBack;
@override
Widget build(BuildContext context) => AppPage(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: "返回",
onPressed: onBack ?? onHome,
),
title: const Text("对话完成"),
),
child: SpacedColumn(
children: [
const Eyebrow('对话完成'),