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:
shenlei
2026-09-16 16:38:23 +09:00
co-authored by Claude Opus 5
parent 0c16394b5a
commit 61abc69037
24 changed files with 2896 additions and 665 deletions
+11 -2
View File
@@ -8,11 +8,13 @@ class AppPage extends StatelessWidget {
required this.child,
this.appBar,
this.bottomNavigationBar,
this.scrollController,
});
final Widget child;
final PreferredSizeWidget? appBar;
final Widget? bottomNavigationBar;
final ScrollController? scrollController;
@override
Widget build(BuildContext context) {
@@ -22,6 +24,7 @@ class AppPage extends StatelessWidget {
body: SafeArea(
top: appBar == null,
child: SingleChildScrollView(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(16, 20, 16, 24),
child: child,
),
@@ -145,14 +148,20 @@ class Eyebrow extends StatelessWidget {
}
class SpacedColumn extends StatelessWidget {
const SpacedColumn({super.key, required this.children, this.spacing = 12});
const SpacedColumn({
super.key,
required this.children,
this.spacing = 12,
this.crossAxisAlignment = CrossAxisAlignment.start,
});
final List<Widget> children;
final double spacing;
final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: crossAxisAlignment,
children: [
for (var index = 0; index < children.length; index++) ...[
children[index],
+673 -113
View File
@@ -34,6 +34,43 @@ VocabularyItem? findCourseLexicon(String text) {
}, orElse: () => null);
}
/// Extracts all distinct course lexicon phrases/words that appear within [text].
List<VocabularyItem> extractCourseLexiconPhrases(String text) {
final normalized = text.toLowerCase().replaceAll('', "'").trim();
if (normalized.isEmpty) return const [];
final matched = <VocabularyItem>[];
final seen = <String>{};
for (final item in courseLexiconEntries) {
final word = item.word.toLowerCase().replaceAll('', "'");
if (word.isEmpty || word == normalized) continue;
final pattern = RegExp(
r'(?<![a-zA-Z0-9])' + RegExp.escape(word) + r'(?![a-zA-Z0-9])',
caseSensitive: false,
);
if (pattern.hasMatch(normalized)) {
if (seen.add(word)) {
matched.add(item);
}
}
}
return matched;
}
/// Determines whether the input text looks like a multi-word phrase or complete sentence.
bool isSentenceQuery(String text) {
final trimmed = text.trim();
if (trimmed.isEmpty) return false;
final words = trimmed.split(RegExp(r'\s+'));
return words.length >= 3 ||
trimmed.contains('.') ||
trimmed.contains('?') ||
trimmed.contains('!') ||
trimmed.contains(';') ||
trimmed.contains('') ||
trimmed.contains('') ||
trimmed.length > 25;
}
/// Inline course text that lets a learner tap a known word or phrase without
/// leaving the current task. Longest phrases are matched before their words.
class LexiconText extends StatefulWidget {
@@ -43,12 +80,14 @@ class LexiconText extends StatefulWidget {
required this.state,
this.style,
this.textAlign,
this.showSentenceAction = false,
});
final String text;
final AppState state;
final TextStyle? style;
final TextAlign? textAlign;
final bool showSentenceAction;
@override
State<LexiconText> createState() => _LexiconTextState();
@@ -135,15 +174,34 @@ class _LexiconTextState extends State<LexiconText> {
}
},
),
if (selectedText.isNotEmpty)
TextButton.icon(
onPressed: () => showLexiconLookup(
context,
state: widget.state,
initialText: selectedText,
if (selectedText.isNotEmpty || widget.showSentenceAction)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Wrap(
spacing: 8,
children: [
if (selectedText.isNotEmpty)
TextButton.icon(
onPressed: () => showLexiconLookup(
context,
state: widget.state,
initialText: selectedText,
),
icon: const Icon(Icons.translate_outlined, size: 16),
label: const Text('查询已选文本'),
),
if (widget.showSentenceAction && widget.text.trim().isNotEmpty)
TextButton.icon(
onPressed: () => showLexiconLookup(
context,
state: widget.state,
initialText: widget.text,
),
icon: const Icon(Icons.psychology_alt_outlined, size: 16),
label: const Text('解析整句与短语'),
),
],
),
icon: const Icon(Icons.translate_outlined, size: 16),
label: const Text('查询已选文本'),
),
],
);
@@ -174,17 +232,25 @@ class _LexiconLookupSheet extends StatefulWidget {
class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
late final TextEditingController controller;
VocabularyItem? entry;
List<VocabularyItem> localPhrases = [];
SentenceAnalysisResult? sentenceAnalysis;
bool requestingSentenceAnalysis = false;
String? sentenceAnalysisError;
bool requestingTemporaryDefinition = false;
String? temporaryDefinition;
String? temporaryError;
final Set<String> _addedToReview = {};
@override
void initState() {
super.initState();
controller = TextEditingController(text: widget.initialText);
entry = findCourseLexicon(widget.initialText);
final initial = widget.initialText.trim();
controller = TextEditingController(text: initial);
entry = findCourseLexicon(initial);
localPhrases = extractCourseLexiconPhrases(initial);
sentenceAnalysis = widget.state.sentenceAnalysisFor(initial);
temporaryDefinition = entry == null
? widget.state.temporaryDefinitionFor(widget.initialText)?.definition
? widget.state.temporaryDefinitionFor(initial)?.definition
: null;
}
@@ -194,13 +260,50 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
super.dispose();
}
void _lookup() => setState(() {
entry = findCourseLexicon(controller.text);
temporaryDefinition = widget.state
.temporaryDefinitionFor(controller.text)
?.definition;
temporaryError = null;
});
void _lookup() {
final query = controller.text.trim();
setState(() {
entry = findCourseLexicon(query);
localPhrases = extractCourseLexiconPhrases(query);
sentenceAnalysis = widget.state.sentenceAnalysisFor(query);
temporaryDefinition = widget.state
.temporaryDefinitionFor(query)
?.definition;
sentenceAnalysisError = null;
temporaryError = null;
});
}
Future<void> _requestSentenceAnalysis() async {
final text = controller.text.trim();
if (text.isEmpty) return;
setState(() {
requestingSentenceAnalysis = true;
sentenceAnalysisError = null;
});
final result = await AiService.instance.analyzeSentence(
provider: widget.state.aiProvider,
endpoint: widget.state.aiEndpoint,
model: widget.state.aiModel,
text: text,
);
if (!mounted) return;
setState(() {
requestingSentenceAnalysis = false;
sentenceAnalysis = result;
sentenceAnalysisError = result == null
? (widget.state.aiProvider.name == 'mock'
? '未能完成解析,请稍后重试。'
: 'AI 解析暂不可用,请检查网络或 AI 服务配置。')
: null;
});
if (result != null) {
widget.state.cacheSentenceAnalysis(result);
}
}
Future<void> _requestTemporaryDefinition() async {
final text = controller.text.trim();
@@ -232,111 +335,568 @@ class _LexiconLookupSheetState extends State<_LexiconLookupSheet> {
}
}
@override
Widget build(BuildContext context) => SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(
20,
0,
20,
24 + MediaQuery.viewInsetsOf(context).bottom,
),
child: SpacedColumn(
children: [
const Eyebrow('课程词典 · 短语优先'),
TextField(
controller: controller,
autofocus: true,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _lookup(),
decoration: InputDecoration(
hintText: '输入或粘贴英文词、短语、句子',
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: const Icon(Icons.search),
onPressed: _lookup,
void _addPhraseToReview(PhraseBreakdownItem phrase) {
widget.state.addPhraseToReview(
phrase: phrase.phrase,
meaning: phrase.meaning,
ipa: phrase.ipa,
usageNote: phrase.usageNote,
contextSentence: controller.text.trim(),
);
setState(() {
_addedToReview.add(phrase.phrase.toLowerCase());
});
try {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已将短语 "${phrase.phrase}" 加入复习计划'),
duration: const Duration(seconds: 2),
),
);
} catch (_) {}
}
void _addVocabItemToReview(VocabularyItem item) {
widget.state.addSavedWord(item);
setState(() {
_addedToReview.add(item.word.toLowerCase());
});
try {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已将 "${item.word}" 加入复习计划'),
duration: const Duration(seconds: 2),
),
);
} catch (_) {}
}
Widget _buildSentenceAnalysisSection(SentenceAnalysisResult analysis) {
return SpacedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Translation
SectionCard(
tint: AppColors.softGreen,
child: SpacedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.translate, size: 16, color: AppColors.green),
SizedBox(width: 6),
Text(
'中文整句翻译',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppColors.green,
),
),
],
),
Text(
analysis.translation,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.4,
),
),
],
),
),
// Pattern, Pronunciation Tips, Grammar Note
if (analysis.sentencePattern != null ||
analysis.pronunciationTips != null ||
analysis.grammarNote != null)
SectionCard(
tint: AppColors.warm,
child: SpacedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (analysis.sentencePattern != null &&
analysis.sentencePattern!.isNotEmpty) ...[
const Row(
children: [
Icon(
Icons.lightbulb_outline,
size: 16,
color: Color(0xFFD97706),
),
SizedBox(width: 6),
Text(
'核心句型',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFFD97706),
),
),
],
),
Text(
analysis.sentencePattern!,
style: const TextStyle(fontSize: 14, height: 1.3),
),
],
if (analysis.pronunciationTips != null &&
analysis.pronunciationTips!.isNotEmpty) ...[
const SizedBox(height: 6),
const Row(
children: [
Icon(
Icons.record_voice_over_outlined,
size: 16,
color: AppColors.green,
),
SizedBox(width: 6),
Text(
'口语连读与发音',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: AppColors.green,
),
),
],
),
Text(
analysis.pronunciationTips!,
style: const TextStyle(fontSize: 14, height: 1.3),
),
],
if (analysis.grammarNote != null &&
analysis.grammarNote!.isNotEmpty) ...[
const SizedBox(height: 6),
const Row(
children: [
Icon(
Icons.menu_book_outlined,
size: 16,
color: Color(0xFF4B5563),
),
SizedBox(width: 6),
Text(
'语法要点',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Color(0xFF4B5563),
),
),
],
),
Text(
analysis.grammarNote!,
style: const TextStyle(fontSize: 14, height: 1.3),
),
],
],
),
),
if (entry == null) ...[
const SectionCard(
child: Text('本地词典未收录。可以请求临时释义;它会标记为待审核,不能加入复习或影响掌握度。'),
),
OutlinedButton.icon(
onPressed: requestingTemporaryDefinition
? null
: _requestTemporaryDefinition,
icon: const Icon(Icons.auto_awesome_outlined),
label: Text(requestingTemporaryDefinition ? '查询中…' : '生成临时释义'),
),
if (temporaryError != null)
Text(
temporaryError!,
style: const TextStyle(color: AppColors.warmInk),
// Phrases Breakdown
if (analysis.phrases.isNotEmpty) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(
Icons.auto_stories_outlined,
size: 18,
color: AppColors.green,
),
if (temporaryDefinition != null)
SectionCard(
tint: AppColors.warm,
child: SpacedColumn(
children: [
Text('待审核临时释义\n$temporaryDefinition'),
Text(
'仅保存在本机,不会加入复习或影响掌握度。',
style: Theme.of(context).textTheme.bodySmall,
const SizedBox(width: 6),
Text(
'重点短语与搭配 (${analysis.phrases.length})',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
for (final phrase in analysis.phrases)
SectionCard(
child: SpacedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8,
children: [
Text(
phrase.phrase,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (phrase.ipa != null && phrase.ipa!.isNotEmpty)
Text(
phrase.ipa!,
style: const TextStyle(
fontSize: 13,
color: AppColors.muted,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.volume_up_outlined, size: 20),
tooltip: '播放读音',
onPressed: () => VoiceService.instance.speak(phrase.phrase),
),
const SizedBox(width: 4),
_addedToReview.contains(phrase.phrase.toLowerCase())
? const Chip(
label: Text('已在复习', style: TextStyle(fontSize: 12)),
avatar: Icon(Icons.check, size: 14, color: AppColors.green),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
)
: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
onPressed: () => _addPhraseToReview(phrase),
icon: const Icon(Icons.bookmark_add_outlined, size: 14),
label: const Text('加复习', style: TextStyle(fontSize: 12)),
),
],
),
Text(
phrase.meaning,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
TextButton.icon(
onPressed: () {
widget.state.removeTemporaryDefinition(controller.text);
setState(() => temporaryDefinition = null);
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('删除此临时释义'),
),
if (phrase.usageNote != null && phrase.usageNote!.isNotEmpty)
Text(
'用法:${phrase.usageNote}',
style: const TextStyle(
fontSize: 12,
color: AppColors.muted,
),
),
],
),
),
],
// Cache source / re-analyze footer
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'解析引擎:${analysis.provider} · 本机已缓存',
style: const TextStyle(fontSize: 12, color: AppColors.muted),
),
TextButton.icon(
onPressed: requestingSentenceAnalysis ? null : _requestSentenceAnalysis,
icon: const Icon(Icons.refresh, size: 14),
label: const Text('重新解析', style: TextStyle(fontSize: 12)),
),
],
),
],
);
}
@override
Widget build(BuildContext context) {
final queryText = controller.text.trim();
final isSentence = isSentenceQuery(queryText);
final hasExactCourseEntry = entry != null && !isSentence;
return SafeArea(
child: Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * 0.85,
),
padding: EdgeInsets.fromLTRB(
20,
0,
20,
24 + MediaQuery.viewInsetsOf(context).bottom,
),
child: SingleChildScrollView(
child: SpacedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Eyebrow('查词与整句深度解析'),
if (sentenceAnalysis != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.softGreen,
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'已解析',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: AppColors.green,
),
),
),
],
),
// Search bar
TextField(
controller: controller,
autofocus: queryText.isEmpty,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _lookup(),
decoration: InputDecoration(
hintText: '输入英文单词、短语或整句',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.search),
suffixIcon: queryText.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 18),
onPressed: () {
controller.clear();
_lookup();
},
)
: null,
),
),
// Audio row for current query
if (queryText.isNotEmpty)
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => VoiceService.instance.speak(queryText),
icon: const Icon(Icons.volume_up_outlined, size: 18),
label: const Text('朗读原文'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () =>
VoiceService.instance.speak(queryText, slow: true),
icon: const Icon(Icons.slow_motion_video_outlined, size: 18),
label: const Text('慢速朗读'),
),
),
],
),
),
] else ...[
Text(
entry!.word,
style: Theme.of(context).textTheme.headlineMedium,
),
if (entry!.ipa != null)
Text(entry!.ipa!, style: Theme.of(context).textTheme.bodyMedium),
Text(entry!.meaning, style: const TextStyle(fontSize: 18)),
SectionCard(
tint: AppColors.softGreen,
child: Text('${entry!.example}\n${entry!.exampleMeaning}'),
),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => VoiceService.instance.speak(entry!.word),
icon: const Icon(Icons.volume_up_outlined),
label: const Text('播放'),
),
// Mode 1: Exact course lexicon single word card
if (hasExactCourseEntry) ...[
Text(
entry!.word,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () =>
VoiceService.instance.speak(entry!.word, slow: true),
icon: const Icon(Icons.slow_motion_video_outlined),
label: const Text('慢放'),
),
if (entry!.ipa != null)
Text(entry!.ipa!, style: Theme.of(context).textTheme.bodyMedium),
Text(entry!.meaning, style: const TextStyle(fontSize: 18)),
SectionCard(
tint: AppColors.softGreen,
child: Text('${entry!.example}\n${entry!.exampleMeaning}'),
),
Row(
children: [
Expanded(
child: PrimaryButton(
label: _addedToReview.contains(entry!.word.toLowerCase())
? '已在复习中'
: '加入复习',
onPressed: () {
_addVocabItemToReview(entry!);
},
),
),
],
),
// Provide option to analyze further with AI if user wants deeper context
if (sentenceAnalysis == null)
OutlinedButton.icon(
onPressed: requestingSentenceAnalysis ? null : _requestSentenceAnalysis,
icon: const Icon(Icons.auto_awesome_outlined, size: 16),
label: Text(requestingSentenceAnalysis ? 'AI 解析中…' : '请求 AI 句型与深度解析'),
),
],
),
PrimaryButton(
label: '加入复习',
onPressed: () {
widget.state.addSavedWord(entry!);
Navigator.pop(context);
},
),
],
],
// Mode 2: Sentence / Phrase Analysis Section
if (sentenceAnalysis != null) ...[
_buildSentenceAnalysisSection(sentenceAnalysis!),
],
// Local phrases extracted from sentence (offline fallback/supplement)
if (localPhrases.isNotEmpty && sentenceAnalysis == null) ...[
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.layers_outlined, size: 18, color: AppColors.green),
const SizedBox(width: 6),
Text(
'本地词典匹配到的短语 (${localPhrases.length})',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
for (final phraseItem in localPhrases)
SectionCard(
tint: AppColors.softGreen,
child: SpacedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
phraseItem.word,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.volume_up_outlined, size: 18),
onPressed: () =>
VoiceService.instance.speak(phraseItem.word),
),
const SizedBox(width: 4),
_addedToReview.contains(phraseItem.word.toLowerCase())
? const Icon(Icons.check, size: 18, color: AppColors.green)
: OutlinedButton(
style: OutlinedButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
),
onPressed: () =>
_addVocabItemToReview(phraseItem),
child: const Text('+ 复习', style: TextStyle(fontSize: 12)),
),
],
),
Text(phraseItem.meaning, style: const TextStyle(fontSize: 14)),
if (phraseItem.example.isNotEmpty)
Text(
'例:${phraseItem.example} (${phraseItem.exampleMeaning})',
style: const TextStyle(
fontSize: 12,
color: AppColors.muted,
),
),
],
),
),
],
// If sentenceAnalysis is not yet available, show AI Trigger Section
if (sentenceAnalysis == null && !hasExactCourseEntry) ...[
SectionCard(
child: SpacedColumn(
children: [
Text(
isSentence
? '需要整个句子的翻译、句型解析、口语连读技巧与重点短语拆解?'
: '本地词典未精确收录。可使用 AI 智能剖析其含义、发音、短语搭配与例句。',
),
if (requestingSentenceAnalysis)
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 10),
Text('AI 正在深度解析句子结构与短语…'),
],
)
else
PrimaryButton(
label: 'AI 深度解析句子与短语',
icon: Icons.auto_awesome,
onPressed: _requestSentenceAnalysis,
),
if (sentenceAnalysisError != null)
Text(
sentenceAnalysisError!,
style: const TextStyle(color: AppColors.warmInk),
),
],
),
),
// Legacy fallback for quick temporary definition if learner only wants a gloss
if (temporaryDefinition == null)
TextButton.icon(
onPressed: requestingTemporaryDefinition
? null
: _requestTemporaryDefinition,
icon: const Icon(Icons.text_fields_outlined, size: 16),
label: Text(
requestingTemporaryDefinition
? '查询简短释义中…'
: '仅生成简短释义 (待审核)',
),
),
if (temporaryError != null)
Text(
temporaryError!,
style: const TextStyle(color: AppColors.warmInk),
),
if (temporaryDefinition != null)
SectionCard(
tint: AppColors.warm,
child: SpacedColumn(
children: [
Text('待审核临时释义\n$temporaryDefinition'),
Text(
'仅保存在本机,不会加入复习或影响掌握度。',
style: Theme.of(context).textTheme.bodySmall,
),
TextButton.icon(
onPressed: () {
widget.state.removeTemporaryDefinition(controller.text);
setState(() => temporaryDefinition = null);
},
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('删除此临时释义'),
),
],
),
),
],
],
),
),
),
),
);
);
}
}