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 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(0xFFEDEBFF), borderRadius: BorderRadius.circular(40), ), child: const Icon(Icons.health_and_safety, size: 40, color: Color(0xFF635BFF)), ), 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(0xFF635BFF).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(0xFF7C73FF), Color(0xFF635BFF), Color(0xFF5241D9)], 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(0xFFE0DDFF))), ), ], ), ), 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(0xFFE0DDFF)), ), ), ], ), ), // ── 快捷操作按钮网格 ── 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(0xFFD0CCED)), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), child: Text('或直接对我说...', style: TextStyle(fontSize: 13, color: Color(0xFF9E94CF))), ), Container(width: 24, height: 1, color: const Color(0xFFD0CCED)), ], ), ), ], ), ), ); } 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(0xFFEBE8FF), 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(0xFF635BFF)), ), const SizedBox(height: 7), Text(a.label, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF333333))), ], ), ), ); } List _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 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(0xFFEDEBFF)), ), child: Column(children: [ CircleAvatar( radius: 24, backgroundColor: const Color(0xFFEDEBFF), child: Text(doc['name']![0], style: const TextStyle(fontSize: 20, color: Color(0xFF635BFF))), ), 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(0xFFF5F3FF), borderRadius: BorderRadius.circular(4), ), child: Text(doc['dept']!, style: const TextStyle(fontSize: 10, color: Color(0xFF635BFF))), ), 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(0xFF635BFF).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(0xFF635BFF) : 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(0xFF635BFF).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(0xFFF5F3FF)]), ), 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(0xFF635BFF))), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: remaining, minHeight: 8, backgroundColor: const Color(0xFFEFEDFF), valueColor: const AlwaysStoppedAnimation(Color(0xFF635BFF)), ), ), // 操作按钮 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(0xFF635BFF).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 : {}; 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(0xFFF5F3FF), 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(0xFFF5F3FF), 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(0xFF635BFF).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(0xFF635BFF)), SizedBox(width: 6), Text('AI 解读摘要', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF635BFF))), ], ), 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(0xFF635BFF).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(0xFFF5F3FF), foregroundColor: const Color(0xFF635BFF), 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(0xFFE8E6FF), width: 1.5), boxShadow: [BoxShadow(color: const Color(0xFF635BFF).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(0xFFEDEBFF), borderRadius: BorderRadius.circular(13), ), child: const CircularProgressIndicator(strokeWidth: 2.2, color: Color(0xFF635BFF)), ), 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(0xFF635BFF) : 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(0xFFE8E6FF), width: 1.5), boxShadow: isUser ? [] : [BoxShadow(color: const Color(0xFF635BFF).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(0xFFF5F3FF)), ), ), if (!isUser && msg.content.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 10), child: Row(children: [ const CircleAvatar(radius: 10, backgroundColor: Color(0xFFEDEBFF), child: Icon(Icons.chat_bubble_outline, size: 14, color: Color(0xFF635BFF))), 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(0xFF635BFF), 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(0xFF635BFF), side: const BorderSide(color: Color(0xFF635BFF), 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 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(0xFF635BFF).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(0xFF635BFF)), 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(0xFF635BFF)), 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.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(0xFF635BFF)), const SizedBox(width: 6), const Text('AI 建议', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFF635BFF))), 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)), ], ], ), ), ); } }