Files
AI-Health/health_app/lib/pages/home/widgets/chat_messages_view.dart
MingNian 8dcf99cac5 style: 全项目紫色→薄荷绿 Fresh Air 清新风
- 主色 #635BFF→#14B8A6 (薄荷绿)
- 浅紫 #EDEBFF→#E6FAF6 (极浅薄荷)
- 深紫 #4B44D6→#0F9D8E (深薄荷)
- 渐变紫→薄荷渐变
- 全局13种紫色映射替换
2026-06-03 20:30:28 +08:00

1319 lines
60 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/navigation_provider.dart';
import '../../../providers/chat_provider.dart';
import '../../../providers/data_providers.dart';
/// 对话消息列表
class ChatMessagesView extends ConsumerWidget {
final ScrollController scrollCtrl;
final List<ChatMessage> messages;
const ChatMessagesView({super.key, required this.scrollCtrl, required this.messages});
@override
Widget build(BuildContext context, WidgetRef ref) {
final chatState = ref.watch(chatProvider);
if (messages.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFE6FAF6),
borderRadius: BorderRadius.circular(40),
),
child: const Icon(Icons.health_and_safety, size: 40, color: Color(0xFF14B8A6)),
),
const SizedBox(height: 16),
Text('开始和 AI 健康管家对话吧', style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 8),
const Text('记录健康数据,获取专业建议', style: TextStyle(fontSize: 14, color: Color(0xFF9E9E9E))),
],
),
);
}
return ListView.builder(
controller: scrollCtrl,
reverse: true,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[messages.length - 1 - index];
return _buildMessageContent(context, ref, msg, chatState);
},
);
}
// ─── 消息分发 ─────────────────────────────────────────────
Widget _buildMessageContent(BuildContext context, WidgetRef ref, ChatMessage msg, ChatState chatState) {
final isUser = msg.isUser;
if (!isUser && chatState.isStreaming && msg.content.isEmpty) {
return _buildThinkingBubble(context, chatState.thinkingText);
}
switch (msg.type) {
case MessageType.agentWelcome:
final storedAgent = _parseAgentFromName(msg.metadata?['agent'] as String?);
return _buildAgentWelcomeCard(context, ref, msg, storedAgent);
case MessageType.taskCard:
return _buildTaskCardInChat(context, ref);
case MessageType.dataConfirm:
return _buildDataConfirmCard(context, msg);
case MessageType.medicationConfirm:
return _buildMedicationConfirmCard(context, msg);
case MessageType.dietAnalysis:
return _buildDietAnalysisCard(context, msg);
case MessageType.reportAnalysis:
return _buildReportAnalysisCard(context, msg);
case MessageType.quickOptions:
return _buildQuickOptionsCard(context, msg);
default:
return _buildTextBubble(context, msg);
}
}
// ═══════════════════════════════════════════════════════════
// 1. AgentWelcomeCard — 智能体欢迎卡片
// ═══════════════════════════════════════════════════════════
Widget _buildAgentWelcomeCard(BuildContext context, WidgetRef ref, ChatMessage msg, ActiveAgent agent) {
final info = _agentInfo(agent);
final actions = agent.actions;
final screenWidth = MediaQuery.of(context).size.width;
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(maxWidth: screenWidth * 0.92),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: const Color(0xFF14B8A6).withAlpha(20), blurRadius: 16, offset: const Offset(0, 4)),
],
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── 紫色渐变头部 ──
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(20, 24, 16, 20),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF2DC4B4), Color(0xFF14B8A6), Color(0xFF0E8071)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.white.withAlpha(30),
borderRadius: BorderRadius.circular(14),
),
child: Icon(info.$1, size: 26, color: Colors.white),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(info.$2, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white)),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
decoration: BoxDecoration(
color: Colors.white.withAlpha(25),
borderRadius: BorderRadius.circular(10),
),
child: Text(info.$3, style: const TextStyle(fontSize: 12, color: Color(0xFFC6EBE6))),
),
],
),
),
GestureDetector(
onTap: () {},
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: Colors.white.withAlpha(20),
shape: BoxShape.circle,
),
child: const Icon(Icons.close, size: 16, color: Color(0xFFC6EBE6)),
),
),
],
),
),
// ── 快捷操作按钮网格 ──
Padding(
padding: const EdgeInsets.fromLTRB(18, 18, 18, 4),
child: Wrap(
spacing: 10,
runSpacing: 10,
children: agent == ActiveAgent.consultation
? _buildDoctorCards(screenWidth, ref)
: actions.map((a) => _agentActionBtn(a, screenWidth, context, ref)).toList(),
),
),
// ── 底部提示 ──
Padding(
padding: const EdgeInsets.fromLTRB(0, 12, 0, 18),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(width: 24, height: 1, color: const Color(0xFFC6EBE6)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Text('或直接对我说...', style: TextStyle(fontSize: 13, color: Color(0xFF9EDBD3))),
),
Container(width: 24, height: 1, color: const Color(0xFFC6EBE6)),
],
),
),
],
),
),
);
}
Widget _agentActionBtn(_AgentAction a, double screenWidth, BuildContext context, WidgetRef ref) {
return InkWell(
onTap: () {
if (a.route != null) {
if (a.route == 'camera' || a.route == 'gallery') {
ref.read(cameraActionProvider.notifier).trigger(a.route!);
} else {
pushRoute(ref, a.route!);
}
} else if (a.label == '服药打卡') {
_medicationCheckIn(ref, context);
}
}, borderRadius: BorderRadius.circular(14),
child: Container(
width: ((screenWidth - 72) / (a.isWide ? 2 : 3)) - 10,
padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 8),
decoration: BoxDecoration(
color: const Color(0xFFF7F5FF),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFD4EDE8), width: 1),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: const Color(0xFFEDEAFF),
borderRadius: BorderRadius.circular(11),
),
child: Icon(a.icon, size: 20, color: const Color(0xFF14B8A6)),
),
const SizedBox(height: 7),
Text(a.label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF333333))),
],
),
),
);
}
List<Widget> _buildDoctorCards(double screenWidth, WidgetRef ref) {
const doctors = [
{'name': '张医生', 'title': '主任医师', 'dept': '心内科', 'desc': '冠心病、高血压术后管理', 'id': 'doc_1'},
{'name': '李医生', 'title': '副主任医师', 'dept': '内分泌科', 'desc': '糖尿病、甲状腺疾病管理', 'id': 'doc_2'},
{'name': '王医生', 'title': '主治医师', 'dept': '营养科', 'desc': '术后营养指导、饮食方案制定', 'id': 'doc_3'},
];
return doctors.map((d) => _doctorCard(d, screenWidth, ref)).toList();
}
Widget _doctorCard(Map<String, String> doc, double screenWidth, WidgetRef ref) {
return InkWell(
onTap: () => pushRoute(ref, 'consultation', params: {'id': doc['id']!}),
borderRadius: BorderRadius.circular(14),
child: Container(
width: screenWidth * 0.38,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFE6FAF6)),
),
child: Column(children: [
CircleAvatar(
radius: 24,
backgroundColor: const Color(0xFFE6FAF6),
child: Text(doc['name']![0], style: const TextStyle(fontSize: 20, color: Color(0xFF14B8A6))),
),
const SizedBox(height: 8),
Text(doc['name']!, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
Text(doc['title']!, style: const TextStyle(fontSize: 11, color: Color(0xFF999999))),
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFF2FAF9),
borderRadius: BorderRadius.circular(4),
),
child: Text(doc['dept']!, style: const TextStyle(fontSize: 10, color: Color(0xFF14B8A6))),
),
const SizedBox(height: 6),
Text(
doc['desc']!,
style: const TextStyle(fontSize: 10, color: Color(0xFF888888), height: 1.3),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
]),
),
);
}
// ═══════════════════════════════════════════════════════════
// 2. DataConfirmCard — 增强版数据确认卡片
// ═══════════════════════════════════════════════════════════
Widget _buildDataConfirmCard(BuildContext context, ChatMessage msg) {
final meta = msg.metadata;
final metricType = meta?['type'] as String? ?? '';
final value = meta?['value'] as String? ?? '';
final abnormal = meta?['abnormal'] as bool? ?? false;
final recordTime = meta?['recordTime'] as String? ?? '';
final unit = meta?['unit'] as String? ?? _getMetricUnit(metricType);
final trend = meta?['trend'] as List? ?? [0.6, 0.8, 0.5];
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── 绿色勾选条 ──
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Color(0xFF4CAF50), Color(0xFF43A047)]),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle, size: 18, color: Colors.white),
SizedBox(width: 6),
Text('✓ 数据已记录', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white)),
],
),
),
Padding(
padding: const EdgeInsets.all(18),
child: Column(
children: [
// 记录时间
Align(
alignment: Alignment.centerLeft,
child: Text(recordTime.isNotEmpty ? recordTime : _formatTime(msg.createdAt), style: const TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))),
),
const SizedBox(height: 14),
// 主要指标区域
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: const Color(0xFFF9F8FF),
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: const Color(0xFFEDEAFF),
borderRadius: BorderRadius.circular(14),
),
child: Center(child: Text(_getMetricIcon(metricType), style: const TextStyle(fontSize: 26))),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(_getMetricName(metricType), style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
const SizedBox(height: 4),
RichText(
text: TextSpan(
children: [
TextSpan(text: value, style: TextStyle(fontSize: 28, fontWeight: FontWeight.w800, color: abnormal ? const Color(0xFFE53935) : const Color(0xFF1A1A2E))),
TextSpan(text: ' $unit', style: TextStyle(fontSize: 14, color: abnormal ? const Color(0xFFE53970) : const Color(0xFF999999))),
],
),
),
],
),
),
],
),
),
// 异常警告条
if (abnormal) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFFFF3F0),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFFFDAD4), width: 1),
),
child: const Row(
children: [
Icon(Icons.warning_amber_rounded, size: 18, color: Color(0xFFE53935)),
SizedBox(width: 8),
Expanded(child: Text('⚠️ 数值偏高,建议关注', style: TextStyle(fontSize: 13, color: Color(0xFFE53935), fontWeight: FontWeight.w500))),
],
),
),
],
// 迷你趋势图最近3次
const SizedBox(height: 16),
Row(
children: [
const Text('近期趋势', style: TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
const Spacer(),
const Text('最近3次', style: TextStyle(fontSize: 11, color: Color(0xFFCCCCCC))),
],
),
const SizedBox(height: 8),
SizedBox(
height: 36,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: trend.asMap().entries.map((e) {
final h = (e.value * 32).clamp(6.0, 32.0);
return Padding(
padding: EdgeInsets.only(right: e.key < trend.length - 1 ? 10 : 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 22,
height: h,
decoration: BoxDecoration(
color: e.key == trend.length - 1 ? const Color(0xFF14B8A6) : const Color(0xFFD5D0FF),
borderRadius: BorderRadius.circular(5),
),
),
const SizedBox(height: 4),
Text('${e.key + 1}', style: const TextStyle(fontSize: 9, color: Color(0xFFBBBBBB))),
],
),
);
}).toList(),
),
),
// 底部操作按钮
const SizedBox(height: 18),
Row(children: [
Expanded(child: _cardOutlineBtn('编辑', Icons.edit_outlined)),
const SizedBox(width: 8),
Expanded(child: _cardFilledBtn('确认', Icons.check)),
const SizedBox(width: 8),
Expanded(child: _cardOutlineBtn('查看详情', Icons.trending_up_outlined)),
]),
],
),
),
],
),
),
);
}
// ═══════════════════════════════════════════════════════════
// 3. MedicationConfirmCard — 增强版用药确认卡片
// ═══════════════════════════════════════════════════════════
Widget _buildMedicationConfirmCard(BuildContext context, ChatMessage msg) {
final meta = msg.metadata;
final name = meta?['name'] as String? ?? '';
final dosage = meta?['dosage'] as String? ?? '';
final time = meta?['time'] as String? ?? '';
final frequency = meta?['frequency'] as String? ?? '';
final remaining = meta?['remaining'] as double? ?? 0.65;
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── 药品头部 ──
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(18, 20, 18, 16),
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Color(0xFFE8F0FE), Color(0xFFF2FAF9)]),
),
child: Row(
children: [
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: const Color(0xFFFFF3E0),
borderRadius: BorderRadius.circular(13),
),
child: const Center(child: Text('💊', style: TextStyle(fontSize: 24))),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
if (dosage.isNotEmpty) ...[
const SizedBox(height: 3),
Text(dosage, style: const TextStyle(fontSize: 13, color: Color(0xFF777777))),
],
],
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 6),
child: Column(
children: [
// 服药时间 & 频率
_medInfoRow(Icons.schedule_outlined, time.isNotEmpty ? '服药时间:$time' : '待设置'),
if (frequency.isNotEmpty) _medInfoRow(Icons.repeat, '频率:$frequency'),
// 剩余药量进度条
const SizedBox(height: 14),
Row(
children: [
const Text('剩余药量', style: TextStyle(fontSize: 13, color: Color(0xFF666666))),
const Spacer(),
Text('${(remaining * 100).toInt()}%', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF14B8A6))),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: remaining,
minHeight: 8,
backgroundColor: const Color(0xFFEFEDFF),
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF14B8A6)),
),
),
// 操作按钮
const SizedBox(height: 18),
Row(children: [
Expanded(child: _cardFilledBtn('确认服药', Icons.check_circle_outline)),
const SizedBox(width: 8),
Expanded(child: _cardOutlineBtn('跳过', Icons.skip_next)),
const SizedBox(width: 8),
Expanded(child: _cardOutlineBtn('设置提醒', Icons.notifications_none_outlined)),
]),
const SizedBox(height: 8),
],
),
),
],
),
),
);
}
Widget _medInfoRow(IconData icon, String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
Icon(icon, size: 17, color: const Color(0xFF888888)),
const SizedBox(width: 8),
Text(text, style: const TextStyle(fontSize: 13, color: Color(0xFF444444))),
],
),
);
}
// ═══════════════════════════════════════════════════════════
// 4. DietAnalysisCard — 增强版饮食分析卡片
// ═══════════════════════════════════════════════════════════
Widget _buildDietAnalysisCard(BuildContext context, ChatMessage msg) {
final meta = msg.metadata;
final foods = meta?['foods'] as List? ?? [];
final totalCalories = meta?['totalCalories'] as int? ?? 0;
final advice = meta?['advice'] as String? ?? '饮食均衡,多吃蔬菜水果,减少高油高糖食物摄入。';
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── 头部 ──
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: const BoxDecoration(gradient: LinearGradient(colors: [Color(0xFFFFF8E1), Color(0xFFFFF3E0)])),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text('🍽️ ', style: TextStyle(fontSize: 18)),
Text('饮食分析结果', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
]),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
// ── 总热量(仅 >0 时显示) ──
if (totalCalories > 0) ...[
Center(child: Column(children: [
Text('$totalCalories', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.w800, color: Color(0xFFFF8F00))),
const Text('千卡 (kcal)', style: TextStyle(fontSize: 12, color: Color(0xFFAAAAAA))),
])),
const SizedBox(height: 16),
],
// ── 识别食物列表 ──
if (foods.isNotEmpty) ...[
const Text('识别结果', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(height: 10),
...foods.map((food) {
final f = food is Map ? food : <String, dynamic>{};
final name = f['name'] as String? ?? '';
final calories = f['calories'] as num? ?? 0;
final portion = f['portion'] as String?;
final nutrients = f['nutrients'] as String?;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: const Color(0xFFFAFAFA), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFF0F0F0))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Expanded(child: Text(name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF1A1A1A)))),
if (calories > 0) Text('${calories is int ? calories : calories.toInt()} kcal', style: const TextStyle(fontSize: 13, color: Color(0xFF888888))),
]),
if (portion != null && portion.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text(portion, style: TextStyle(fontSize: 12, color: Colors.grey[500]))),
if (nutrients != null && nutrients.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 2), child: Text(nutrients, style: TextStyle(fontSize: 11, color: Colors.grey[500]))),
]),
);
}),
] else ...[
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: const Color(0xFFF2FAF9), borderRadius: BorderRadius.circular(12)),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.hourglass_empty, size: 18, color: Color(0xFF999999)),
SizedBox(width: 8),
Text('正在分析食物中...', style: TextStyle(fontSize: 14, color: Color(0xFF999999))),
]),
),
],
// ── AI 建议 ──
const SizedBox(height: 14),
const Text('AI 建议', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A))),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: const Color(0xFFF2FAF9), borderRadius: BorderRadius.circular(10)),
child: Text(advice, style: const TextStyle(fontSize: 13, height: 1.6, color: Color(0xFF555555))),
),
]),
),
],
),
),
);
}
// ═══════════════════════════════════════════════════════════
// 5. ReportAnalysisCard — 增强版报告分析卡片
// ═══════════════════════════════════════════════════════════
Widget _buildReportAnalysisCard(BuildContext context, ChatMessage msg) {
final meta = msg.metadata;
final reportType = meta?['type'] as String? ?? '体检报告';
final reportDate = meta?['date'] as String? ?? '';
final indicators = meta?['indicators'] as List? ?? [];
final summary = meta?['summary'] as String? ?? '';
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── 报告头部 ──
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(18, 18, 18, 14),
decoration: const BoxDecoration(
gradient: LinearGradient(colors: [Color(0xFFE8EAF6), Color(0xFFEDE7F6)]),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFC5CAE9),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.description_outlined, size: 20, color: Color(0xFF3F51B5)),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(reportType, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1A1A2E))),
if (reportDate.isNotEmpty)
Text(reportDate, style: const TextStyle(fontSize: 12, color: Color(0xFF888888))),
],
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 指标表格
Container(
decoration: BoxDecoration(
color: const Color(0xFFFAFAFC),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFEEEEEE), width: 1),
),
child: Column(
children: [
// 表头
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: const BoxDecoration(
color: Color(0xFFF5F4FA),
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
child: const Row(
children: [
Expanded(flex: 2, child: Text('指标名称', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)))),
Expanded(flex: 1, child: Text('数值', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)),
Expanded(flex: 1, child: Text('状态', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF666666)), textAlign: TextAlign.center)),
],
),
),
// 数据行
...indicators.map((ind) {
final i = ind as Map? ?? {};
final name = i['name'] as String? ?? '';
final value = i['value'] as String? ?? '';
final status = i['status'] as String? ?? 'normal';
final refRange = i['refRange'] as String? ?? '';
final isAbnormal = status != 'normal';
Color sc;
switch (status) {
case 'high': sc = const Color(0xFFE53935); break;
case 'low': sc = const Color(0xFFF9A825); break;
default: sc = const Color(0xFF43A047);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: isAbnormal ? const Color(0xFFFFF8F5) : Colors.transparent,
border: Border(
bottom: BorderSide(color: const Color(0xFFF0F0F0), width: 0.5),
),
),
child: Row(
children: [
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: TextStyle(fontSize: 13, color: isAbnormal ? const Color(0xFFE53935) : const Color(0xFF333333), fontWeight: isAbnormal ? FontWeight.w600 : FontWeight.normal)),
if (refRange.isNotEmpty) Text('参考:$refRange', style: const TextStyle(fontSize: 10, color: Color(0xFFAAAAAA))),
],
),
),
Expanded(flex: 1, child: Text(value, textAlign: TextAlign.center, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: sc))),
Expanded(
flex: 1,
child: Center(
child: Container(
width: 8,
height: 8,
decoration: BoxDecoration(shape: BoxShape.circle, color: sc),
),
),
),
],
),
);
}),
],
),
),
// AI 解读摘要
if (summary.isNotEmpty) ...[
const SizedBox(height: 14),
Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFF3EFFF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFDDD8FF), width: 0.8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.auto_awesome, size: 16, color: Color(0xFF14B8A6)),
SizedBox(width: 6),
Text('AI 解读摘要', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF14B8A6))),
],
),
const SizedBox(height: 8),
Text(summary, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), height: 1.5)),
],
),
),
],
// 查看完整解读按钮
const SizedBox(height: 14),
SizedBox(
width: double.infinity,
child: _cardFilledBtn('查看完整解读', Icons.article_outlined),
),
const SizedBox(height: 4),
],
),
),
],
),
),
);
}
// ═══════════════════════════════════════════════════════════
// 6. QuickOptionsCard — 优化样式
// ═══════════════════════════════════════════════════════════
Widget _buildQuickOptionsCard(BuildContext context, ChatMessage msg) {
final meta = msg.metadata;
final options = meta?['options'] as List? ?? [];
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(15), blurRadius: 14, offset: const Offset(0, 4))],
),
child: Padding(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(msg.content, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1A1A2E))),
const SizedBox(height: 14),
Wrap(spacing: 8, runSpacing: 8, children: options.map((opt) {
final o = opt as Map? ?? {};
return ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF2FAF9),
foregroundColor: const Color(0xFF14B8A6),
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 11),
),
child: Text(o['label'] as String? ?? '', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
);
}).toList()),
],
),
),
),
);
}
// ═══════════════════════════════════════════════════════════
// 公共组件:思考气泡 & 文本气泡
// ═══════════════════════════════════════════════════════════
Widget _buildThinkingBubble(BuildContext context, String? thinkingText) {
return Align(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
decoration: BoxDecoration(
color: const Color(0xFFFEFEFF),
borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
border: Border.all(color: const Color(0xFFD4EDE8), width: 1.5),
boxShadow: [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 26,
height: 26,
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: const Color(0xFFE6FAF6),
borderRadius: BorderRadius.circular(13),
),
child: const CircularProgressIndicator(strokeWidth: 2.2, color: Color(0xFF14B8A6)),
),
const SizedBox(width: 10),
Text(thinkingText?.isNotEmpty == true ? thinkingText! : '正在分析...', style: const TextStyle(fontSize: 14, color: Color(0xFF999999))),
],
),
),
);
}
Widget _buildTextBubble(BuildContext context, ChatMessage msg) {
final isUser = msg.isUser;
final imageUrl = msg.metadata?['imageUrl'] as String?;
final localPath = msg.metadata?['localImagePath'] as String?;
final hasImage = imageUrl != null || localPath != null;
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: isUser ? const Color(0xFF14B8A6) : const Color(0xFFFEFEFF),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isUser ? 20 : 4),
topRight: Radius.circular(isUser ? 4 : 20),
bottomLeft: const Radius.circular(20),
bottomRight: const Radius.circular(20),
),
border: isUser ? null : Border.all(color: const Color(0xFFD4EDE8), width: 1.5),
boxShadow: isUser ? [] : [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (hasImage)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: imageUrl != null
? Image.network(imageUrl, fit: BoxFit.cover, width: double.infinity, errorBuilder: (_, __, ___) => _buildLocalFallback(localPath))
: localPath != null
? Image.file(File(localPath), fit: BoxFit.cover, width: double.infinity)
: null,
),
),
if (isUser)
Text(msg.content, style: const TextStyle(fontSize: 16, color: Colors.white, height: 1.4))
else
MarkdownBody(
data: msg.content,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: const TextStyle(fontSize: 16, color: Color(0xFF1A1A1A), height: 1.5),
h1: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
h2: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)),
code: const TextStyle(fontSize: 14, backgroundColor: Color(0xFFF2FAF9)),
),
),
if (!isUser && msg.content.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(children: [
const CircleAvatar(radius: 10, backgroundColor: Color(0xFFE6FAF6), child: Icon(Icons.chat_bubble_outline, size: 14, color: Color(0xFF14B8A6))),
const SizedBox(width: 6),
const Text('健康管家', style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E))),
const SizedBox(width: 4),
const Text('仅供参考', style: TextStyle(fontSize: 11, color: Color(0xFFCCCCCC))),
]),
),
],
),
),
);
}
Widget _buildLocalFallback(String? localPath) {
if (localPath != null) {
final file = File(localPath);
return Image.file(file, fit: BoxFit.cover, width: double.infinity);
}
return Container(
height: 100,
color: const Color(0xFFEEEEEE),
child: const Center(child: Icon(Icons.broken_image, size: 40, color: Color(0xFFBDBDBD))),
);
}
// ═══════════════════════════════════════════════════════════
// 公共组件:通用按钮
// ═══════════════════════════════════════════════════════════
Widget _cardFilledBtn(String label, IconData icon) {
return ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF14B8A6),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 11),
),
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(icon, size: 16),
const SizedBox(width: 5),
Text(label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
]),
);
}
Widget _cardOutlineBtn(String label, IconData icon) {
return OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF14B8A6),
side: const BorderSide(color: Color(0xFF14B8A6), width: 1.2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 11),
),
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(icon, size: 15),
const SizedBox(width: 4),
Text(label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
]),
);
}
// ═══════════════════════════════════════════════════════════
// 工具方法
// ═══════════════════════════════════════════════════════════
String _formatTime(DateTime dt) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final thatDay = DateTime(dt.year, dt.month, dt.day);
if (thatDay == today) {
return '今天 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}
if (thatDay == today.subtract(const Duration(days: 1))) {
return '昨天 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}
return '${dt.month}/${dt.day} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
}
String _getMetricUnit(String type) {
switch (type.toLowerCase()) {
case 'blood_pressure': return 'mmHg';
case 'heart_rate': return 'bpm';
case 'glucose': return 'mmol/L';
case 'spo2': return '%';
case 'weight': return 'kg';
default: return '';
}
}
String _getMetricIcon(String type) {
switch (type.toLowerCase()) {
case 'blood_pressure': return '🩺';
case 'heart_rate': return '💓';
case 'glucose': return '💉';
case 'spo2': return '🫁';
case 'weight': return '⚖️';
default: return '📊';
}
}
String _getMetricName(String type) {
switch (type.toLowerCase()) {
case 'blood_pressure': return '血压';
case 'heart_rate': return '心率';
case 'glucose': return '血糖';
case 'spo2': return '血氧';
case 'weight': return '体重';
default: return '健康指标';
}
}
static void _medicationCheckIn(WidgetRef ref, BuildContext context) async {
try {
final service = ref.read(medicationServiceProvider);
final reminders = await ref.read(medicationReminderProvider.future);
if (reminders.isEmpty) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('暂无待服药记录'), backgroundColor: Color(0xFFFF9800)),
);
return;
}
for (final m in reminders) {
await service.confirm(m['id']?.toString() ?? '');
}
ref.invalidate(medicationReminderProvider);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('打卡成功 已记录 ${reminders.length} 项服药'),
backgroundColor: const Color(0xFF43A047),
),
);
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('打卡失败:$e'), backgroundColor: Colors.red),
);
}
}
static (_AgentIcon, String, String) _agentInfo(ActiveAgent agent) {
return switch (agent) {
ActiveAgent.health => (Icons.favorite_border, '记数据', '录入血压、血糖、心率等日常指标'),
ActiveAgent.diet => (Icons.restaurant, '拍饮食', '拍照识别食物热量和营养成分'),
ActiveAgent.medication => (Icons.medication, '药管家', '管理药品、提醒服药、追踪用量'),
ActiveAgent.consultation => (Icons.local_hospital, '问诊', '在线咨询医生,描述症状获取建议'),
ActiveAgent.report => (Icons.assignment, '看报告', '上传体检报告AI 辅助解读'),
ActiveAgent.exercise => (Icons.directions_run, '运动', '制定运动计划,打卡记录进度'),
_ => (Icons.smart_toy, 'AI 助手', '您的智能健康管家'),
};
}
static ActiveAgent _parseAgentFromName(String? name) {
switch (name) {
case 'consultation': return ActiveAgent.consultation;
case 'health': return ActiveAgent.health;
case 'diet': return ActiveAgent.diet;
case 'medication': return ActiveAgent.medication;
case 'report': return ActiveAgent.report;
case 'exercise': return ActiveAgent.exercise;
default: return ActiveAgent.default_;
}
}
Widget _buildTaskCardInChat(BuildContext context, WidgetRef ref) {
final health = ref.watch(latestHealthProvider);
return health.when(
data: (data) => _taskCardBubble(data),
loading: () => _taskCardBubble({}),
error: (_, __) => _taskCardBubble({}),
);
}
Widget _taskCardBubble(Map<String, dynamic> data) {
final bp = data['BloodPressure'];
final bpText = bp is Map ? '${bp['systolic'] ?? '--'}/${bp['diastolic'] ?? '--'}' : '--';
final hr = data['HeartRate'];
final hrText = hr is Map && hr['value'] != null ? '${hr['value']}' : '--';
final gl = data['Glucose'];
final glText = gl is Map && gl['value'] != null ? '${gl['value']}' : '--';
final sp = data['SpO2'];
final spText = sp is Map && sp['value'] != null ? '${sp['value']}' : '--';
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: const Color(0xFF14B8A6).withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [Icon(Icons.today, size: 18, color: const Color(0xFF14B8A6)), const SizedBox(width: 8), const Text('今日任务', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Color(0xFF1A1A1A)))]),
const SizedBox(height: 10),
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
_miniMetric('血压', bpText, Icons.favorite),
_miniMetric('心率', hrText, Icons.monitor_heart),
_miniMetric('血糖', glText, Icons.bloodtype),
_miniMetric('血氧', spText, Icons.air),
]),
]),
);
}
Widget _miniMetric(String label, String value, IconData icon) {
return Column(children: [
Icon(icon, size: 20, color: const Color(0xFF14B8A6)),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFF1A1A1A))),
Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF999999))),
]);
}
}
// ════════════════════════════════════════════════════════════════
// 内部数据类
// ════════════════════════════════════════════════════════════════
typedef _AgentIcon = IconData;
class _AgentAction {
final String label;
final IconData icon;
final bool isWide;
final String? route;
const _AgentAction({required this.label, required this.icon, this.isWide = false, this.route});
}
final _agentActions = <ActiveAgent, List<_AgentAction>>{
ActiveAgent.health: [
_AgentAction(label: '录入血压', icon: Icons.monitor_heart_outlined, route: 'trend'),
_AgentAction(label: '录入血糖', icon: Icons.bloodtype_outlined, route: 'trend'),
_AgentAction(label: '录入心率', icon: Icons.favorite_border, route: 'trend'),
_AgentAction(label: '录入血氧', icon: Icons.air_outlined, route: 'trend'),
_AgentAction(label: '录入体重', icon: Icons.monitor_weight_outlined, route: 'trend'),
],
ActiveAgent.diet: [
_AgentAction(label: '拍照识别', icon: Icons.camera_alt_outlined, isWide: true, route: 'camera'),
_AgentAction(label: '上传照片', icon: Icons.photo_library_outlined, isWide: true, route: 'gallery'),
],
ActiveAgent.medication: [
_AgentAction(label: '用药管理', icon: Icons.medication_liquid_outlined, isWide: true, route: 'medications'),
_AgentAction(label: '服药打卡', icon: Icons.check_circle_outline, isWide: true),
],
ActiveAgent.consultation: [],
ActiveAgent.report: [
_AgentAction(label: '上传报告', icon: Icons.upload_file_outlined, isWide: true, route: 'reports'),
_AgentAction(label: '查看历史', icon: Icons.history_outlined, isWide: true, route: 'reports'),
],
ActiveAgent.exercise: [
_AgentAction(label: '本周计划', icon: Icons.calendar_month_outlined, isWide: true, route: 'exercisePlan'),
_AgentAction(label: '新建计划', icon: Icons.add_task_outlined, isWide: true, route: 'exercisePlan'),
_AgentAction(label: '今日打卡', icon: Icons.fact_check_outlined, isWide: true, route: 'exercisePlan'),
],
};
extension _AgentActionsExt on ActiveAgent {
List<_AgentAction> get actions => _agentActions[this] ?? [const _AgentAction(label: '开始对话', icon: Icons.chat_outlined)];
}
// ════════════════════════════════════════════════════════════════
// 可展开的 AI 建议小组件
// ════════════════════════════════════════════════════════════════
class _ExpandableAdvice extends StatefulWidget {
final String advice;
const _ExpandableAdvice({required this.advice});
@override
State<_ExpandableAdvice> createState() => _ExpandableAdviceState();
}
class _ExpandableAdviceState extends State<_ExpandableAdvice> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xFFF7F5FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE8E4FF), width: 0.8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.lightbulb_outline, size: 16, color: Color(0xFF14B8A6)),
const SizedBox(width: 6),
const Text('AI 建议', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF14B8A6))),
const Spacer(),
Icon(_expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 18, color: const Color(0xFFAAAAAA)),
],
),
if (_expanded) ...[
const SizedBox(height: 10),
Text(widget.advice, style: const TextStyle(fontSize: 13, color: Color(0xFF555555), height: 1.6)),
],
],
),
),
);
}
}